From b8948bc4958855c6e342e18bdb8dc81cfcf09455 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Sun, 5 Nov 2023 09:38:45 -0500 Subject: [PATCH] fix things --- .gitignore | 3 + AHITClient.py | 236 +++++++ data/yatta.ico | Bin 0 -> 152484 bytes data/yatta.png | Bin 0 -> 34873 bytes setup-ahitclient.py | 642 ++++++++++++++++++ worlds/ahit/DeathWishLocations.py | 262 +++++++ worlds/ahit/DeathWishRules.py | 539 +++++++++++++++ worlds/ahit/Items.py | 286 ++++++++ worlds/ahit/Locations.py | 977 +++++++++++++++++++++++++++ worlds/ahit/Options.py | 728 ++++++++++++++++++++ worlds/ahit/Regions.py | 900 ++++++++++++++++++++++++ worlds/ahit/Rules.py | 944 ++++++++++++++++++++++++++ worlds/ahit/Types.py | 80 +++ worlds/ahit/__init__.py | 334 +++++++++ worlds/ahit/docs/en_A Hat in Time.md | 31 + worlds/ahit/docs/setup_en.md | 43 ++ worlds/ahit/test/TestActs.py | 31 + worlds/ahit/test/TestBase.py | 5 + worlds/ahit/test/__init__.py | 0 19 files changed, 6041 insertions(+) create mode 100644 AHITClient.py create mode 100644 data/yatta.ico create mode 100644 data/yatta.png create mode 100644 setup-ahitclient.py create mode 100644 worlds/ahit/DeathWishLocations.py create mode 100644 worlds/ahit/DeathWishRules.py create mode 100644 worlds/ahit/Items.py create mode 100644 worlds/ahit/Locations.py create mode 100644 worlds/ahit/Options.py create mode 100644 worlds/ahit/Regions.py create mode 100644 worlds/ahit/Rules.py create mode 100644 worlds/ahit/Types.py create mode 100644 worlds/ahit/__init__.py create mode 100644 worlds/ahit/docs/en_A Hat in Time.md create mode 100644 worlds/ahit/docs/setup_en.md create mode 100644 worlds/ahit/test/TestActs.py create mode 100644 worlds/ahit/test/TestBase.py create mode 100644 worlds/ahit/test/__init__.py diff --git a/.gitignore b/.gitignore index f4bcd35c32..b9ca4b8d28 100644 --- a/.gitignore +++ b/.gitignore @@ -60,6 +60,7 @@ Output Logs/ /installdelete.iss /data/user.kv /datapackage +/oot/ # Byte-compiled / optimized / DLL files __pycache__/ @@ -196,3 +197,5 @@ minecraft_versions.json .LSOverride Thumbs.db [Dd]esktop.ini +A Hat in Time.yaml +ahit.apworld diff --git a/AHITClient.py b/AHITClient.py new file mode 100644 index 0000000000..884f3ee5c7 --- /dev/null +++ b/AHITClient.py @@ -0,0 +1,236 @@ +import asyncio +import Utils +import websockets +import functools +from copy import deepcopy +from typing import List, Any, Iterable +from NetUtils import decode, encode, JSONtoTextParser, JSONMessagePart, NetworkItem +from MultiServer import Endpoint +from CommonClient import CommonContext, gui_enabled, ClientCommandProcessor, logger, get_base_parser + +DEBUG = False + + +class AHITJSONToTextParser(JSONtoTextParser): + def _handle_color(self, node: JSONMessagePart): + return self._handle_text(node) # No colors for the in-game text + + +class AHITCommandProcessor(ClientCommandProcessor): + def __init__(self, ctx: CommonContext): + super().__init__(ctx) + + def _cmd_ahit(self): + """Check AHIT Connection State""" + if isinstance(self.ctx, AHITContext): + logger.info(f"AHIT Status: {self.ctx.get_ahit_status()}") + + +class AHITContext(CommonContext): + command_processor = AHITCommandProcessor + game = "A Hat in Time" + + def __init__(self, server_address, password): + super().__init__(server_address, password) + self.proxy = None + self.proxy_task = None + self.gamejsontotext = AHITJSONToTextParser(self) + self.autoreconnect_task = None + self.endpoint = None + self.items_handling = 0b111 + self.room_info = None + self.connected_msg = None + self.game_connected = False + self.awaiting_info = False + self.full_inventory: List[Any] = [] + self.server_msgs: List[Any] = [] + + async def server_auth(self, password_requested: bool = False): + if password_requested and not self.password: + await super(AHITContext, self).server_auth(password_requested) + + await self.get_username() + await self.send_connect() + + def get_ahit_status(self) -> str: + if not self.is_proxy_connected(): + return "Not connected to A Hat in Time" + + return "Connected to A Hat in Time" + + async def send_msgs_proxy(self, msgs: Iterable[dict]) -> bool: + """ `msgs` JSON serializable """ + if not self.endpoint or not self.endpoint.socket.open or self.endpoint.socket.closed: + return False + + if DEBUG: + logger.info(f"Outgoing message: {msgs}") + + await self.endpoint.socket.send(msgs) + return True + + async def disconnect(self, allow_autoreconnect: bool = False): + await super().disconnect(allow_autoreconnect) + + async def disconnect_proxy(self): + if self.endpoint and not self.endpoint.socket.closed: + await self.endpoint.socket.close() + if self.proxy_task is not None: + await self.proxy_task + + def is_connected(self) -> bool: + return self.server and self.server.socket.open + + def is_proxy_connected(self) -> bool: + return self.endpoint and self.endpoint.socket.open + + def on_print_json(self, args: dict): + text = self.gamejsontotext(deepcopy(args["data"])) + msg = {"cmd": "PrintJSON", "data": [{"text": text}], "type": "Chat"} + self.server_msgs.append(encode([msg])) + + if self.ui: + self.ui.print_json(args["data"]) + else: + text = self.jsontotextparser(args["data"]) + logger.info(text) + + def update_items(self): + # just to be safe - we might still have an inventory from a different room + if not self.is_connected(): + return + + self.server_msgs.append(encode([{"cmd": "ReceivedItems", "index": 0, "items": self.full_inventory}])) + + def on_package(self, cmd: str, args: dict): + if cmd == "Connected": + self.connected_msg = encode([args]) + if self.awaiting_info: + self.server_msgs.append(self.room_info) + self.update_items() + self.awaiting_info = False + + elif cmd == "ReceivedItems": + if args["index"] == 0: + self.full_inventory.clear() + + for item in args["items"]: + self.full_inventory.append(NetworkItem(*item)) + + self.server_msgs.append(encode([args])) + + elif cmd == "RoomInfo": + self.seed_name = args["seed_name"] + self.room_info = encode([args]) + + else: + if cmd != "PrintJSON": + self.server_msgs.append(encode([args])) + + def run_gui(self): + from kvui import GameManager + + class AHITManager(GameManager): + logging_pairs = [ + ("Client", "Archipelago") + ] + base_title = "Archipelago A Hat in Time Client" + + self.ui = AHITManager(self) + self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI") + + +async def proxy(websocket, path: str = "/", ctx: AHITContext = None): + ctx.endpoint = Endpoint(websocket) + try: + await on_client_connected(ctx) + + if ctx.is_proxy_connected(): + async for data in websocket: + if DEBUG: + logger.info(f"Incoming message: {data}") + + for msg in decode(data): + if msg["cmd"] == "Connect": + # Proxy is connecting, make sure it is valid + if msg["game"] != "A Hat in Time": + logger.info("Aborting proxy connection: game is not A Hat in Time") + await ctx.disconnect_proxy() + break + + if ctx.seed_name: + seed_name = msg.get("seed_name", "") + if seed_name != "" and seed_name != ctx.seed_name: + logger.info("Aborting proxy connection: seed mismatch from save file") + logger.info(f"Expected: {ctx.seed_name}, got: {seed_name}") + text = encode([{"cmd": "PrintJSON", + "data": [{"text": "Connection aborted - save file to seed mismatch"}]}]) + await ctx.send_msgs_proxy(text) + await ctx.disconnect_proxy() + break + + if ctx.connected_msg and ctx.is_connected(): + await ctx.send_msgs_proxy(ctx.connected_msg) + ctx.update_items() + continue + + if not ctx.is_proxy_connected(): + break + + await ctx.send_msgs([msg]) + + except Exception as e: + if not isinstance(e, websockets.WebSocketException): + logger.exception(e) + finally: + await ctx.disconnect_proxy() + + +async def on_client_connected(ctx: AHITContext): + if ctx.room_info and ctx.is_connected(): + await ctx.send_msgs_proxy(ctx.room_info) + else: + ctx.awaiting_info = True + + +async def main(): + parser = get_base_parser() + args = parser.parse_args() + + ctx = AHITContext(args.connect, args.password) + logger.info("Starting A Hat in Time proxy server") + ctx.proxy = websockets.serve(functools.partial(proxy, ctx=ctx), + host="localhost", port=11311, ping_timeout=999999, ping_interval=999999) + ctx.proxy_task = asyncio.create_task(proxy_loop(ctx), name="ProxyLoop") + + if gui_enabled: + ctx.run_gui() + ctx.run_cli() + + await ctx.proxy + await ctx.proxy_task + await ctx.exit_event.wait() + + +async def proxy_loop(ctx: AHITContext): + try: + while not ctx.exit_event.is_set(): + if len(ctx.server_msgs) > 0: + for msg in ctx.server_msgs: + await ctx.send_msgs_proxy(msg) + + ctx.server_msgs.clear() + await asyncio.sleep(0.1) + except Exception as e: + logger.exception(e) + logger.info("Aborting AHIT Proxy Client due to errors") + + +if __name__ == '__main__': + Utils.init_logging("AHITClient") + options = Utils.get_options() + + import colorama + colorama.init() + asyncio.run(main()) + colorama.deinit() diff --git a/data/yatta.ico b/data/yatta.ico new file mode 100644 index 0000000000000000000000000000000000000000..f87a0980f49c3cf346af8c288bab020eb77885c6 GIT binary patch literal 152484 zcmXV11ytP5*WF!gaV_p{g+)toXK@xN#oeV)+;y?CxD<*kE~U7&rMMM$ic2Y4ytsb+ z&iBtrPBO_klQ;7+@7>(F0{{R4=z#w=Ab=hqJO%*RK3|81{(qSr32jn0CJ^la61OPyqBnUw8eEzRKdsF~GToVL{)zVPH#iGP|t{PVv zDzE+D)Bg_ef3G%nFMob#C_q_WM%Q=o;NdEjSvg}THb{52+ykFBnQ%~mE6)1#bM=08 z-hSTE9z%dwYZ6O?$TG;M(=`RQ*d|zcfwH zLERyw9a|Y8kt)w>{T8jH$Xq!z^5Nre3XPGOnV$i-Kd;&_20YdNx1V1yoxagr&jfhT zr35el1AcO;Q-VVw!3xrBk31@E=Wl~72hOdh>j2m8Q0f!y&}a)cuEzs_0X^~4=s*Zs zRDn>`w`^%_d#lV2biv=ph81f>GRdS^1Kk^$p;o|?a*iMguYED>o4Ifbt`g7zpdZL*5amci(U9PvC}(rMeqTLs*@3d?+g@^Mk}{ORbpCYyZy{N&ll9> z$qk^TVd457VR6I_!9;*IPwyz!K~bj?2>MLKm9ij!B9TR>rt7X)CuF)tUW7Tk0A*(L z?F%NHBTLjCQAP4)^yaWO-&RA%9IKZb6lKqrty^1=|LXfPK}?QLho*A+*K^;aaOmUj zAYJiI%X_;92mVH?vp$RzY6+1++t!4+OOfvl&{6N>Ux$b@0pjoHp%s{3XJSaO5 z#|Lb3Tg@L=N~Fcw?8RC!nH2)wdioiOBv`87NgC{Gi?PA*#$-KohE4YTBzlsc@?213 zwCxkha-p{imE&=S9<$)kS~-$ta1pD;JR^?LZiL?(^N~;Tm9Hs~cXXC|4?>7RH9%~-SFpfRG0LkkiY7!q#r#iqn@n&|A zF$w%07n3M5s8u=B=tpe}F8qO^bpteKF1db+E*}+NZ=mYU^^r((x`Hwl&^lKEI7afK^f=k57O4>D2|Pz#r?^J3 z7mK+j`=f6@U-*gUQXXnV>e+qF*Lh`&0{8@t1~o!yxG~LGVevbV?sXf4@N-v#t_g}L zsIkL%W9OfWF)o&6NCIeQfGbEW*nlXB62lFMJ6F2{ktG!&L(lHQflW|SYMrR13liao3m0$V&P z7GyQUgC~#-EH6~H3BB{6PK-49Kn7+?8Let!IId`}1Z5SjcjYhp7N2qI6-@@QcQ%Bt@>z*Wcs&iNC^QDVI@$qX@B(Ygq!44Uvb>Kt6OQF!B;+%J0*$Cl zgsoF*d{yGY;!Tc!P^@E6tV+-ZyRGb}&z%FEPcN=_$mM_60eofm{+0vT5fUM^zQor{ zr*|-fG!$oFFa@85hWhyfps^@)3;QCbGL~JsJ`OwI`T3P8Y8Eam9%s=Bb@EifpO(ej zh|08x+~o_tx(gc@6&@@KL*6EJM_BDv13lGpUpz^#kFcNMRfS= z?#+QFv^lxAlD!7lZ>LG_9ZW_>c9WMKHuNbr%Bd6Hbk8aV()%9ALH=|S-5Rq5cC2CVAk zgnhNP>zCSApHz)nrG_^3);3jwxy|89Aw$&(%+IHPKU+u&oqG`g9K@dZPiQ6jrDTxt zuCk9Ao^A&@C%KFUH6!|u;#(s*JlE5(IjZS)lMHPYR26z|c--Bg0NZMO-16Wq1pFhC5M5H@ zNERH{DT|qi5F4_8#3{O&CTW;fdtuhu61SL=N2?!4%6mQxe@VB_#&mCD>B}@`&S+& zUE&|tcR@Fa^`DU=F`{XwLcDHB@>r|evpoaSXLk-i@tm}Fw)`FsVwVW3i;yUj??PG% z`hRU|m>fNIa6(Crva(TcI~^nl!Idqp9r}-)}Nq=gf&ONl#YZz=Z>Fffl;w6YYJWm)Sv<+4>A)U)c%!^A>pydiM?x149v37wA#}HkR5CLXhof?*4Ws_5=&ZR zOw4VkU=MO$&ixGEcjRvLEW%d??{sx~CtivYVcWr0_|($K=RTicoeXw}Uv28>CY_`sH5-q{MbBXlVXWTFvg)lho#n zh@e>SDA@AtWoV!{yW4OK`SGtENfgn#;|wjMTM6J-GC0Mb7CEv36`S`jT8nXRnRAe- zF2c17m^c_|Sel5ZehyjrgZ3Oq0I~jlwAXn3b5H2~!}xq}l^NK5b<{|BAaDG`DyHHP z+hdGympK)Qz`nql;dDuaL!U%DdH9+ipao> zh&|;Q2X;go+?e%2LPmH%EHA0kExre45i`|548F>9h+#v!%R2~;ih@L^V?~tKF6|pA zqS(bh3!KL2bc}NqZiZMs1uHYJXWk&){1;KBfvF*n_@PgH5&<1PvO}yp>inI#Y9<1S$l^kiM<#Fz!=HYE0 zBH=I$4*Jdbq~S4br)naK8FQknK{vV!!o;Jd&*=0tcDMPDAz$a|HjX^T@HeN4G9?dhv{q{Lo&pk$DbS)m&W94 zaWdYArJ#$EsarR6Au8H-^>B`_bPirpJn$JcS`+ujupMMfwH5I?W*M6az?VGg$x4lM z4RxjSn<6;OX@_DMxw-u3ZY0gDU*~mxcA3YT=9bGQz_f&HnZj)yKUc7bmtlxH9Sq(z&X}h&?jo)%qfQ= zAbRk(0=yAZh-3Wx5zT9Y9yglW7*~zCdhX+G>D8h1+k}V3HuBOVd=X%3l6A`^>y;Me z)Ey1v;1M4qm855YTIKtzUw<4`@OAOv>NQ8?Sj6aRpV{c9iB&WU0Tbi|gW2S*Pe#Q+ z7FjR@v(?SN97?~`tSK9nEQ8d}qKgRozN?&=uLBkBAB@E&!ri*s;xj#O90gUV0T45#0JKed$%i`Oa#T``ROZ26eIWVoq2Qb#X1B zKA)B}8=f@s`7cm@I+Y&KDXwXwHaYh6x)j4~?%sOcg*Y-UC+nKDY+#M$uR0?hXj^}jMqOi-xa0SoPH)1bRAPksD_uWL^IF9E z<1vtiw5K@$;=oA&E%F1f&wR%8V3 zW{=|!&{nHktleiTDRBNh@t_}AMHm=R4;Lk6{TaCyL+p%iE<`@!dGYOqbtsh-{WGcW zj%(Kl^xxI%IbZQSfb`+7oUGD=+%mt1bl3^~Iry$8<5+r|-ypIs8gsr-(lQ$n!AzLD z`K6OBvt0$4B+k5s_K50>v55ib^ij&>Z10S$ICghG$1QH$tEpQuil_5@Au0L~{}%x7 zbEV(-4UP*y_6cyM`#?;oz2dWmlfu95a1laqO)L>gcXE;wp84h43xW0wx+_Tlw2CWF zR)`fd#^CUmTd=V_%g^VG#F0B{OI0=2gs(>iy=NR~P*(|~A!}_jXERt&(yYCk`I4uZ zQ6M_(P`0#oPN4Z@#4Rg%NcQ{@-C&+&={>(rAK759Mu%xfo06*1M)n44KfP??yJW38 zO{(DeSZaEf?}N}{EQ0;C48C$E8q0rDaDwD+SxhR9v>4A!s0PwBb>hD)RY?ReSQk8@ zQJco2Banqs4_lR!)}O zh`@r=-M#UC4V^bNMMXE_L};x2HtLE=!F7k1jHr#3b5sRG=#_h!Xd@`&&& zG>%j4G3&`)$I78C-0OY@1bB&iZF;P@MFp;hH4Iu2YJ6i zz!as{Rdj5pQct!VPGcj$m#9#EKoH(OLd68RTzAS8eI!fW4C`3X}`IH+dRtH>KofC&%+^Dx6>~z$)3Vb7pyYLgVO z$2Qk5a=+R?bFKRQD(1_pG5rn3)Dkk-P+M8|?VDs;-L+$nV++@-kV!dFasgA%fYZZ{ zFMAF+a`ZTI<-K~Q>^ejDHb)v*rcwg5JC*4ad%dr3cbf=VQJT%CL(N1lB6v2pvAMAx% z%I!Obj6bCeO6!sj%(_qIR$bgr^DTnOJ{6XJpDU;Mc=@J@WBB zNszp!!xcy%&|(gae=3N;B?B3rGkQF5BS~ooVF{UAuSRlqsRAe3f6*6`>%SPcqd%A@ z8<~h+i7VDCRD>oCDa1&=?Cb%DJ;!kP)W;2!%ubPqRp42#|K}?4=TyjgwlgTkG+%e? ztKXGySAx-l8^aVxQ#p18n2OP*i?1dS@odHz-H>_XB@v1Zv6VsI(R(2pbs&U>7*_HA zP)r8-1Jq?Ufcxp%e}nivHUQ1I+xpIgF)SDCp!ak(oF?EA)V#T=|3EBCdH0iH6I|FO z((xlCiX{5w(Vgwfgp&u@Sm69|g?0YB@QDbsAPyH}vBZ)e`Urc7MJIo0N%o(~;VUbD z=(9`ANh7WUa5r(LK=NZWR`utgGPd^E5^+J;(8k5?4*>qr>i*8m!$?RYCE1-;+d{;|0a;ZO{%uPeeJk_%waU2;74?HQGlZC{QS_X3raZZ%FD0SyDX^BG$I>KZm(P3Z|(|d?bpc>%#aY8&aWzLEGcwh49fFR+Vlg`7N3((D z#7IENAT;DoFr^$3Mq9qxX1AF8U|!eIb(OqkU7y4TU?PBaVX3Ef3?-3HJLFu(^nVc+6>J7nBp88} zn`q~zBZY~As~gE-7|5LhLEYx!)L2>qXSnpxS2?Ya`I#<27%dq0`sxYWqds08zQvdW z@;_4dDA)v}GAmcyT|yJk)X23gIPgny8qom~xa$_h?aqhvJ>ZoJZ{DAE6er+a z74~QpJkgElD!XV}mkEq~2HAd% z&fd_(T!#og|75_Uj#!-akLwqQngpUbcvy$AkMNFYl>^nBc*phD>TKSz*n=08lZN@p z{R$i}xzqbnD0D+4rHL@vJ&=gIj>qXE0|UnJ1bWBB{H-nD2rJz6f;|`(AZ+lT6bgda zBd&z{Y*l14@}J01BHcBcfiHefD~*uq@gpY%j@@IC`)TTLxxL&<771rAD@LG%_PINV zNO1i@9AF5REg>j~ZWeGmlM;|xzNu;7_XxS&gKYp%`}@Tr!k)Ifu47EZH2DO2?2pcC z15eqKXwj@B*#Ai>??9ZVmLt4`WIunkazp0XSg3`{1E{ke6Q6z-8-rug7-xiG%oAOt zs78tZZi9k$VGaP0N5nW#C)8W z{1MbZQ7_2G8d_({9Q&eZ)7S4+^wY#pXWJlkj=2ENda7kFs&rB_39W&y%=tY{&lLLa zikPF$BBh4BiX(JbL7yB}W`Rj^OqJd7rr)b$&21RjhOXnkWm< zCqEgFBY)Y6Mj)#-GNS#A@O@OwF8sXr+)C-Hjbg$iF4nartrLrAPtE-aTH49$JH8H- zVz{Q-g9g8;5Ej5KuD_Uc!EoKjSeYJ)49dC}-=*%4`SUWA`;d@XG1B^A1$XblzyPwI z>mBJog!-V^G}5hQiuzU5<{@zi)pFu-(EQa^Hh%}*ZmRgB9HYMF$*&&rOUa=iFR4}h z4ji$ih)gr)6B;7%_uBW3Z~4V#2LTL>+zYW3As-7{$I3gPa|@+^s`&TEQ$4)D{q_Cw z<<;P#x;*b(Rwh8%rSmy&qfQn*|9Pn0hu|?#!{wn=!)FwOxqS`8U@w+7`;JT;Gh)8ibG(Pv9KWPRj%#fg`xEo4LO=8P5bJSYn9WKFw5$6{M%5d{t~Y!OM%tBh2q~%*2%kMkLJ_Ews}#i;9zj+MVmaJ_Bjp{^bBlfHxZDehLUb$# zBGK5?KA8T6yB28DK;kPYO_er*-H!KIt0Wozhzt5eI9I@gKg#WTjgINbyYCNS8?6ys zI1=A)^-;8>36(IRHa8ZMR6=InZ-P`6t#c&mFL_@3qz>MzYmiiHU+N8tF3s0R*}bW? zrZQ@TF~-z|cn(Y$dKLi(cfz=ABY3RVH{OPBGl^YT`Lp}$10mruPq`@EJ!pg~Q1>x# zb-DtkQ2Dx~A?2UCu%ZKAoZIy#jJ~AF+k{hTzQ>+siC$D_4nkG-8*~r+Qczn-pT8T1 zPE3y`|D1^=St*l5K#7N;Y@xJ@0OtKC2elX_)&fYFqH^RMQ1-8k{ex8zb5c6>p#6Gb zVkB)dnW@igqNYmR*OfWKej?cetG^L7N}SPBE_xGU-kkKBg=b|n$r4gO29>q=yPKBS zDb|;S`sUgbMi`kxdC|qe{$fuc}@uCCGu-;;;yHovu<_$o3=jtBknvCM31Z)DX3P8 z*X*k90rKPVA##a`!JKriHzm=MK>btH){FDj?MpG-X^l>cU#bF)J>706kS?2BSe!WdQdglLYiF#f$QN;cfA zh(Vw=3v$UaPZ(NRh|1=Ecsc@A4LmVZPg|Tro~iY6&!Y1Ze@DYETI{6`1?B8#o{j&F ze{zacJdBdIjNt2dVc?zqsiaW2~t8D?|?BiS0jpLfM1LjLi5yu{+RC8WP9S+ICW3077B> z-e#X3L!~E@5;nLhlI!DL(0sj92hXs!mPM^@-t?bwrQN?Y=KNY8R<#K377_YZHWtT> zQ*oSJH(7bxdhDbW<{IJD2q08gQKvzmpNK}?_=NUHtgTr2lbqLr*3nON5QbMYYGW-Z z`Cjr>O)}&Pa9A`1M{9`bIKH4bD4+1U{3Gfl8wigp#H+*V_-Pph6}L#{z2L~?uK?cf zPW&dmo#wJB1R#!152|a8RAbvj*;3iA8i{FyT)G1w#Cn zyKKVkL=22G_7>u|wrYfIE-T~lF%R@0V>AOQcOLzEO$)-zz73lZj|PCVc^(o&eNv<} zx;VOO^aU`0JoXW&*iE+ik^jm#78)%m*81fzIBbrg`gz8am?8fPH*4)^-4NIisgxUmnA#k??y2i(DEwS(b{1W-^|2NTlgi z7UEke<0H?O^lXn74&^vVNwx%&?xmw45dLsg5gL~8F_hBww3aVU@l2eyh2SpbzZm@q zjgvh69mYT1fw3-T#S+XS()deKnHfcJlzT4DU$pObu3!B)r^c z6^fW32FmX0JH&(%aDW;X<0dj+FzQ`Q<4LGBc3y)DgXNT|Qme^K$w^!=wY3W)6_{Ug zv)njK=1;-5AF-ZEKla_gdw-f~nOnxv%mdH+Po3Zo%;{f!Y9Fb5)bOGB*r+OvQ{c^s z8^*`#aM#84i2#ls=*A5*^VVLA>p|KTTm+{LvzPQEg;B!J0*DQ)^PPxH`wF zBBx3#cBBU#_7YSbn(MLWH$2mFVB9&cjTYVFhUr!^N>+PmRu=d(7X^ zZq`Vx7{Y)IEEs2R9Z0;aZy`%TL!+8sqPbt+Ar4_S;9;~2?gke^PM0X8@%T+=ZLBlJ zrh)>OY*wvr8Q%-Bu9tdVM5O()KuV+regxwy({nev=y1#D(4+=_C#};yBnAxT!~!fyA4U%Mg%HrI-G$McI7KDXR6Wlt1Y-_X(s4y^76&%Xnyv+I7G z6$EiG)(7Wmw-4#RkKUWaUH$4CDO;MbDG1ZUcv&bCW_Kd)#Vd(CGVom&-<&nYia}@* z6z=%Ucic{+JTZqd5mM>N(wE3$hQ-4Y`jwo|vV;@DZFlm>w9WGrsH|vlmRR%I*gs|3 zIngkI^+L^1eE7!t-XJpEmgBDp{AVtw*L{g1NbXrrqC4?-MZu%n?K;+__gEhPkzLQB zd4w7^gdvz|g?tnYJd;Ro%wEfz0$9J*Iq<=w75x3+37M*6I>{rmU*C~7f>xZiv6kG^ zHt75{7V-O-{F^7zIsV5|uMhErF)AZmWL8*lol<~SokT$%CU_FThv|7Hi6B#~ir*(d z4Jskn;Q(=$Mc%2 z>%DZ*;Z_%E0yuxW-Zm{-uH!iUB%#I3fd_<=kCpfZbF$hw5A^Lb?z8PBa>Ko5Il;7L zFdVQi!#7SiFep)~0^*o16L`o4!%s1FLys+}#VH~vztlJ1p;^on&{%qMi#h+1-murE!*6Y_9asGUs~RS@9K&}S{@@4!HqtC=p|Q4P?a09Dgv~}m z#ui7>hTTq{yc_KH`KKN>PjbTL<-qCfUjcRg$Jj zm~>^ELR+002^y+Jwq6;hcO^F5B9*?J5U!|?D)Vwq_dAQB?!7ytpx;4FeuK!C@H=i` z%p?{mpJ5lC*}oh(Z>3!T2J!I&ekOK%By7MGgq4g}(b5d=Cfy}i;}FP@N<7ZkAZ#Z5 zYj*E{ef2Ed2yWr441rrk|7q}2R25T8balJw7Q$Mf%vRxU~@=AQHAyVKpQq)qXjFBi(~ZMA0eT7T*Xan`i4|RdgCysKMR#R-~0k{ z+PE1dZ_QseaWDTKiwQK`4p$bWmk-p;Xh%2+_tRF?*V+qnYyAXPw{6siOmJ zr!U&B)kIOapDz@`Yn4fwTNhDGaKBXkCHehJ83(Y3yZ-7~B30J8Lmr>6p7QTrsHlc9 zYTxoh%ny`%CWNar(OqHsc_z2%@B8QZ*!XrC1`kWRP;UtMM@X*ZJcjv^M@OPes$R%N zr;jC)&b(tINhNPDJC+_X5~C7{AaTseHQGKO;TPEan}+?OKFe^8f}Kn7{r#Wi#@)!1 z?k2bfn0OsS+ z?A#5lum7|;7MR>u0`mD-X5Zx8-OPx?e3+mBrziQoRHcR$#`5w{*w&QL1TEARI+PYR zR}f3O2dF#-e#oh=uz?c-g`&w)=AnQ_7&9_<8h5V zthW1?7S%N;YG~)YQxW;afz*IKXgZXafPG7QXoOGa`$$&oRVIxeTFioB*}VGl5(eb! z0}P61oZQ$VF}R~SZ(Q~$V`zL1`jjzUV7zSOZ5jWM?PDhON=ar-@@ro6QBqIGx}xnp&SUNTofhA&OC zMh`)MsnF)$VmZ6)l+Q|B?n};=DmAzeqka~9wGvO?AMTNVw9!T@z;a7-k$g1EJ`)rp z7vB#SiTHqH@8j*@ zi#r&6Z*%XcCuqJda^l(NtOKtyb4_jDImVw(@qBZ}7{JrwOqiVZh{<|?Tw}f3#W0Op z%uLuE+34KZNa&5ZaSig6t3!*PA*!u z_{X&^5;gU6j?SU0ZyKw~CXklebzzf(?;O@&BYJLI0@d$P%pw6$c0y1{Td4m>R69B8R-gG*#lhp)Bb%AGy~cUdBjPlR5^JjfBuZ{v$6i7 z;5xRfBp4>W+NE-zuOm89*W82zl(Zr05*XEZq*Oi;9M(2pa@m=rJ zmrZuo;|!jeo87M!PL0|qHKP}aAsZYy>8r{hbC4wJpL%Q7RkYtSwvo?nqg-c`HeydWLC zphNgzbv-O&EX;-{gdZOuVONhn>HP5|q$+-2H3+2y;*nV_NHNtqH@xAqXW=qzh5<*6 zLn8p9y^$iry?6N5QBH{!(G{>?L zMuxj-Bmus3p_FdWlFDVr-|U{8)iq2&LK5sZc~Su%Jv_K_a!l731E60hUyR(W(+uT_gt%@0HB%mgKapUlU?lku>F~vRJ_~B4 zzcJogMGK3i#KI>BjLln?sylr4fW1@nDKBDRK3WFW;0-S8K=Zf2Q`V1Tt6P80(Thqr zn`w%T!@4~cB@s8HI0+&;E|!-=ilUOAwPbCvp}){<*866o#x;a`?$6kfrn#FR>2h`& zsMrh)>1@hA6-{0OD&MN<*VcUA2r^l``*t5oKr^p|cetB*|H3}m(VEdyb8mvaSeiYo zo+P2N)&zSXStOzRJva5Q?tj!D`Y;t0HMD87#v8>O64LeM`0qpXlt2}2Z73~93) zebU=vRI!j0lo^PXE2S~4;xsi)9geT7Kf}y6`sL8noQh({S9LVrf1E8IK@qL_2>>W^ zT6GP2e&CBB~^iKumOJu+dNVvx~d!l8QBaHPIhN)`HDfR~52ThJ#x53@2LOqky2 z?G@eGCKZur=>9WL8flEeF(nCy?F%sfSGhzLkU>c36$-$W#rS!}@Bvg*pQ>|~Y zYpBbzu&_xRuR)pzv5jl6s5WgA!=o(Fmjdwf9(twvBcX$fXYBE%ae9|*DO}p} zEx!5tV;PwBr7A$CfK=X)KC<6KG!5&`*hux+na8WlEd8RS%0k_k1WWl9l-zfYynN*n z>=99Epgq5GXVN`CFw3MqYJpstRfEpyH*cq$*0`d&w;^ecD{L|HIX30*xg}6-<~zzA zDhT~!=qb5_WjQw>2I;NzoDFTv2N$w2a^=sdP(+Z-_|kkxHha^D1upMoqejCa9afL3 zq4=!wlqz!zinPgIQ!ty9C+w-X3p2}5A1Y(~NjA zl^8Nu`9vuyP_;R@l{R)ryfOh_=OsW&u{kIQSIexU)jKc3KAkMGo36@5g6+Pe3MokrAMwMJ%>&a@P+fEogG66wSO?byxH zoZL%TfcRL14>~%j3ErSF!+x7?*ghdmWnHlQa)%qDe%AL1;8~zIULq04Gw4^7J*X{ix!-}G5RfwytUY?u8UmvTAsgS*R+F{b+6?joy zG#C9)FFrR{Oz_hZYM6t9eNHJ}mReR|aEkURYPF6cM|A!)BaW!a`_h0n)pLGP(=kYN zAYj|}iv@vUK&JjEoxKS0o;@?zXl!oPOH?LR!~Dm$diI|pOeSP?yHRK8{1@nxcd|^8 zil1YQOS@*~S;;iV2h8&%&K9CzHy1^g_q0>E9LdemwI=W_2I^ms_LK4!YR(rQt6m~Y zwm>b%3H5E+R%I!6KD?>~{p9}1#?`*+)Y9A^P;3d1k3cG#B%U@+4yZc>omo}|>*5Uw zS$>){N34yqv(fJxEGa9CR?-Y6!VGCcP>H;FP8fzyxbUHOyth-Lty7|-SA=a)w4&8V zTdR-$LD{)0tuueKJw+ve`P@h~Z}@ZjH?4LkIFNl_9?q_71+caK7xF@mAvCG|d6|H5 z7dPi#ddhH4})iJw$i71+RLrZ z=Dm6gPbZ?{aed~OLz}Oe_uB>~Nxm9j3^cfz{2SVKp&pRs>~WGwBVi}pWoN_@gImP; zyTpxFQ8~M;+1mLeUo<%hW{b`IU7E3yAwaJ>3Qsb2;&4veTK7Z4igeHzYm*TcQ>WAc3%_H;?UlrPN$-xemCO;u7QH-QenP$1dQ#2)w!Eh4Np4H63UWY zO)HL%&}cZUW4e;*&8tSc%A|0!&9gouW09ubgBj7jK(vZ*68liR zF8QuO(2OZpRN@pA@}nY+n3rJDnl4JFe(Dw91+@^U2PPk=U~&_jwHg<@i`?_3Qb_RcO(XW?IWPNK0>|j;b2s zAZ2(ancpA2A6$r`nuNtol}^WpC%Gh3G+(LD&1;j19IuBU?ff{#=bANJOTbLAM-glN z3H#O#>W&v)uXeLQr(tx5emE?X4ffoz+E_~sr~IVBV_ojf&PrZA-rCAcEh+DYMGF_c zOgX=C5aiOWG&25ndAzV-P;6+BU>1ie6RlUKK_H0vyo6BZr#04*(Y#R9E0x|2JH+R)&} zG@F?0V3n$x1a`v+-!KWn>uj~YQ6uB$1y#uMsBDE}W=1=s}21FdV^aF$QBGvJ!?@t1$ z23l&lxgJe=E8D_UFl6Fv8&sOUcWuwnczb8eS8?l9d~w@3;;oo%v2VEqFL`z6V}coJ z2w5($_g+i%1?9*T;~P)BC>@?1&MYlOTBuFj7LfhsC@eDogc5>`GQ;sExG)|-T3AIH z%ge1Y4Q;AbO$l~(neIC$UvRWJ8%eFL<5E*AbEKMjZ*){g5H(CCN7c6uQ6Z3RP0MAF zg+X~BEjMF&DZ+P-Q!{+dGUt^^UW^?_ERnCVux0WXREbWjO?5O z!qQ8|^$i}v<42bcbV(~_g)3&~CSk^fETbEI`O;X^(OSy?e(~3065Bx4jZkE)O_j6G z+KFuLcr~7vtX?mXOHU|~Cm9|>$+kK2%S-^Fb)%mS(V!|G^2k$WJ4eCshM>@FOH=IIQFh8LP&T~&87lIBL~|fS4fme@ zy*Z9${8ZR^pG!^UMB#{wgw+3*|Jb|1KB1hRBY$|TbHTSCV%GMAD!{A#k345xX2(m= zOO%6wC%t#P^(#fRGlLxKao>JWN``a7EZui(a5!neEX%vq$JfKd$hv0l+*Y4p)`cwY<#CQb+^d5^i~ue>nXVhI?&< zGW0H*9D0I82??7-r}e41#HcNmIaP|Fx|v)#uX(`Xx6 zzUF=!mbz)N7Qm%$jUZ5FS&3#|X}Boa$xboK!#9ocSFdZ|Zh#Y9%mutxVtb z_j|c4BbJ1K^4+v1rN7`7(Rxmaqg7&Gz66f_o*Llh4*I!xi@Sf}(p@VzQ5}Yd@3HMb z@BZ>!9{qhOJTMq%V`2UE`l5sZc|QewaLXB9`-{H~o9m_i=gvji1L1-ftk8*|0hJH) z%oO2IZ0nqD%b{g@1mf6~(w{`hqt`J`zrA>7-{Fd_7Bd#D-{Gc{p55xkSFatbX!x^M zIlV-17iTzJWH{jU1XFGXD9zhms3P1fNM|L8WY#RRbMYoGx>LaO}K>HBD$7XjmE znqEEPy6JGtrShld{rANxHAf(4>_Dm^ilRr&ZHqQ6r#Jt6%79JYSleo^7Wd2-Guv?Q z`ASn}MK%3q^X=51Q8@!wrzYgp7rY&qfGP%Wk6TWN0 zcy1rD9JDUmO%lkq&%?RduiLL1f9|_ysPwVyp64AMF(Jh-dg=+U_>Lie zG$?EMo$pW@4mUd-UP2k+-R>`)2j;I#=lX{Iu3t-S^}VuU)%XUDmWRn#HEXoB_Z=M( zWoN%KFuaY4zc4yD7}2SEH?c>)uIE~}<{CeMsmBl_9RX&lvYcd+}Fuw zx#q?KK=v20SfleJ{R#yrB?_7{4)X`+Oye|A2iX%e|EuDIbruF2LmGxkHuFUx#kGu)~;im%!hpKhN zc`luRqFsl0I4SSWIMmM6EZ4YHQ(oGf*5d{D_YI4Pw8z)$h9smzHc?^G-Z*`^$S0xa zF3>lD31a7+8A>OgEhXBHZPz1&)d?f7)efDw0IFz* zu+e~e0Idi*@uetdQkOD)fPqu@EO0F-`7oJ-qHiw8Y8CdH5QTl{X^F{9eHBx>ZW9w8 zY&`l$_{*UutsuywrV7Sxo5$Rw^O!;pGK`?niun9)g`3MYf-quw)MLEl9AeQWt82n+ z!gS8b6_B|cD2Je9QLj0C@X-`M`pGoE{P_f5d{$z2$0G`QKo>BpRT^C)gp7+`^vIY4 zDZfDab2>-1Z<>PwBP9g-dIv_S^NG+xn}Qm?L%y72bY_%^*-`SPEMCsVc5PEZSoaPq zC3HG%nyo4u8#lOh>&JZXn?L2}KmR9u{>8sxe&N^b?kv)2H;KaiiFHDl72CIA#E)AQ z8v9*KAw;Q=Yrp^A8`W1XUyLTkMy*=C>2x|BkyJqjL??vpx>4a>PM3JUi5BN`he~y!rnXVJb*q|wHON{N?gBHW^==( zUbE1V$xk2dYUsrpdgUt;YrtR+5;i>&^%vUjPx9%@#WiWdNd}&>C052GTh8L;JiLsD z?K%jHxFM;I%W`X^kc4qjjaqGk#l??UUipmmjq7Y}-Da<{LZh)wt5c8b@9Ca(gJC#F zfn!0*gOLp69l>r(a(gQ@ZU-swT#M%~o_7Di-~GYJ>60hKUZrmR_M@x(*}wT~uH9Y` z8(X_{0t598wej#@+k|!m@0@~yZ;p{@D@nztw5ue28>E1VJY0&*Z4M&X=nwiCc%xZN zp2zedT;VA#9Qv#a0O+L@w}C&ybf0|(Q~a?M1bBNvz}34obhJ*^Q~d6m38yXSb4JZ7t88+Xe32F0j0uW2fTMX-jk%x4KB8a-@rl#LmC? zbGmn-*(EkF;?IW)fnk1v!GxV|Rbj9!f#q1(o`aKh@$wnGyk}N=O97fthw*Nq!QuyP z_V!lT+goLOXMxR)n=CDU#OBr=YPAiT%?d%#G|fr}Jo=6@A-cS4m_Fy2)jI1K5By49 z(h4I&t+6d7^4SdUzWIuJ@zMo(^29Xlb|9w4Cuq0YNcBtFof~X!@0v5y#*@F@gkTFw z88|a)j^o6-t~@{kNG_egfHX&QFx!F68f?|Ok%Y8!rG)8akyH_WDxHB}2LS-)rZq9q z0S9BZ)qt0yh-pA5U?#0rA+zObE13t7F<@-M*dY z*3BaG^LbWRGc@Yv(kd}fXK-L~PABN-zVDOY_PhG-9UvR#>5o!7X@nF=DN#z|XDr;D zk5};Vaz1X>L)jKW7^Gbg1abAH4()c8W^Wvn)Mw4c<)h$la3ZNarXKQo??pW5$fqPg>>5H6kj5#n{f@>SZO-=iD z^Ds!kGRFeosUVFB6aE}y4lH19_XMCKtwpsF@Y^f9RBHjPc8KpPUcKNLk5mdWD$s;l zQmr~HtrWO+bBYgtJ4U7A&}vzPq3Ejn!+dsW4d5uf3HLMnVHth8f75RJZxgDtYzsHz z;^%yFBR+m1gY7v;%M>>0H6k*|y(sEXtFE)XeTTKRD=aR2$QNJyC5^@w?RFz}>J4{< zO92_nc-l|oP1tQLXViYzGQy$OHet9g)->C-fG7e>Ny^0>FFbdaH(z^+e9lxl78;b4 zy!qNo2uoo(Hk-Q@ZrqyRcL^4DjeEWw84X}GdkFIn&f78bE&Ij{X!oC@g{c*o;VI^$ zr*`5GK>@}wxQ(&j>KLr|M4XH!Z!FaKvmdV#go=z?;M`mRY>BXJB0QQ^kE=Ilm|q+* zDY^?;s(ViNVN%L)kZE|>tHGB}p6xw%_r~fEWqy&QG>L)15<5Q?M~@70P@u6hKH1R% znPLt<>l@WSYn!S%@lvTHqak$KEo${`c6OGycI}5O%>SCz)i2oIzDu)N>Du+fW~dB0 zk5M0{a^^LeHEO@_nlM}vw1^^DufhB-Ebk>(En>7>;FXsy@OS>!?=m`CMk(1neIkve z6whBci|e_Zn4RK(|6lw!tgLR(Xf_F>sCyY!D$wj0=iqnF8sQ_8fQcBAolWn0DhZx# zUQ&_{_Vu}GzU&A1QPS5@0Dy7WR4{$v{uFZ)-_kG@n#Eq+UTyHRPwPyM!1PoZ2SKyt zv%Oc~*4y}dl_Y^{l$v3#&c}urpK}0ewmfO_Xqs>zx@fjx6-xNqX?=^SlEVZ z>)>0E_rMZH00jH4FJ0__rD83mAFh$M)Pnf+R5}3R>ni}jm;>7wz|b59HCT)^S%69{ z;QB&~AAcC|owqD}5B6#q7FWi&dVPkC4Ua}s(h1DnKd3}lg5)HwrXN0HZ@jMO zKu3rum98#KZG6{v@v}aD&d1I9xOpGtSV*P1NjX8MMXOz<-KtToZE)+>Pr6Mrws#iT zSieEFwlUC2r#CMpje4Ef_IcO5pUjz%o@bc^oy7T;EaZ_kK%4np1H9R880@|!pt@#?$8N3*h(=wHO}vU=Up0&CQ`zuS8uXct9OM!JAmB= zT;DM2{9MV*QKoaX2Cj9Yp|nGjPY~_DNgYfMN{y$ubI{jW03@`C@e7>9G>6Oqgpp>u z67tEl79U^hkoBRmD_L5~vA9&gFq&!bDevu;OVNG-pbv%S-eW8cLWi-cAE2c~N`-Q4 zyu43tq(~;8!OePDo{fw_2a!%H>vY(yEU~q9mz|wOHaBnb$*2E{ot=5w?K(lwA_&_1 zj+`*Ql4fNudvK}@(|NPHW^D74q6?EsPZ=xo!Z4!U>doi*}_G)#m+_*y!_H_161b4Tg z;Ktx0Y3AC;@X0Mo&#__h1cLo*d;?Q1Y?G(BPQbAMFt>aS6U(h&%8moj4m2A(0e|^p ziDQF8AtTwf?H_dpNZO#(3^y>7s=YEwFFG2b6n56fE9S|Le^L4`0c-7b@huFn%kh!*xKjGPm`43NE0ic$iZ}M-xoady-KCx9+WoG3XI+V z#Twk)5_IB>JlnQ7F+IV5_82$EPU~PSy&#v6$)6cH6w6e~P+w;^L zErKY*vTa^^{v0nqe~vd_ex6*$=b!$oztT5vEyfK%d(nYw8@*NirD<@Teoafqmn({( z(ttagP;Kqg00f5dZ(*Ft#{%FfCJ)nx@G_Q7UAy!gr832;l;bNp=}TkR*Qx z!hBH-Nbf=k2B5f7_lkQY_Po{+Q5va|fZvIbS{qo9)(9n0j*aEJWXDGEOGTWlkL|h! zt%oLzf|&6mYSj%^mOo{F;REIuKVW(JQ+BuS5CkoBTvcZn^GjiD=bUS7`sqBJD8})7 z+o<@1jXJxCyVQ38Ee)#;xVa6VtqUS$W5o>_ONBf?_}zE;*0)|~YH|!AG~2t|{PNcy z^Rr)n!iS$;=lZP$wstDCS}meT$LLAHFF*K{6Vnr%nw_FtDscJIMK(8fsn+TQfjOs8 zn=HV)I|fLZ@!J3jnj?zYbi)gmnOL ztPDkeimB2?EKxR1E4AzyhX;}v@-7{QAI#G0ZD8v6o5m(`0KX$+PrlHm0EToOoScuF z&*BzxWJ*OW&qZ05DIyXdv(s)+t8KBnyU6_AUopS%0c-0w*xs3^R^K9M?j3kON*E{D zhz}DP!|=s8aVGEf!gzz4O4^Z@W~@lVRukrT;Lf&Dt%YSaDCBdTKX;OEz56<2V`Vg& z)wNBoT)ECa{r~+XpIy7n(#i(Ado^0E_C6pqQpaUrsId_8b{CTDd&!eRM=#JWJqXQgl{|j`_c^C@^DL~qmiQ^aZc%?jEA%~mK zVta19mnMorf}lmMvCa1OJnQS%SzZ2wo40<<+Qx0_&0T_^b6txBnz{pY9fX>Cqe**ZoxQy(jb@vS=aI`~Xtx8xMx!e<+Hu}M zsLgz)i$=RqvFTE28s=Z!GZ=*dN|&NrR`01}fG`h85zfRW?}OL>gt&M;jv z%|{J5<`G-`2W0CeU3_}XpCla_b>GQl@J9+{#>&{fhvhhNW}c1{03$j( z8=J6PF^nbRJcM#F&&w~I=lk!!&DqnlY;NuF^Iv?(pZ)6}^1~nhip`x$D)ebacN`nn zbx}&9BTXj=Xtz6rVaV=Ym3p(q#OMgNWs%GHwAyWgpeH~&kr4u)EE|WQqri8Jvu|++ z=C@&^+LsQT+_eU91yeD2haNomsid!)07zDSiNSHd2mAvJoKp%Uw#qP9euJ}(e zoQx@D-3dCh!##p_00^qpHP+UyvN-=+Zr%DBw{QP!AZ|Zw#ozzzdKR26!V42{E*6W) z?nzbRbiQK3_(`ZQRR+-jE@HL+gC!I1ZwIl;Ed=N1rumb<_lNx9@4d(67cQ_ezr_Ff zzx?O?>w9%LB=Q8}>ci-jh z*I(qs^aPD&iy!{vH~jk_|B8jBRU)miq-1((jEiSaa{0yQc;%(%IDhUGW90(A=b?lo z3{1{Kt)Xc0 zA3{o)6+UAd&;E83q7=|Y07cit%YA!DByZ5xA4DUksM0dbbZg6a(`y~G@;kQ0=tzl6 z=TGy!_rJlVi)ZnDkCn9ze)RJXxP5n-APAWl8{x#{81KG$nHQcr$Hnt!ICJ_06XRtp z6_@-; z^?$n&uc%T`DCD?s?iAnu&bz$z=4DF7JiC=Dw{9=++mF8>ib5vF$~=Gm4BvSDMgG?B zeVdu72?~WAj%)X>{}93Jdmhu1W1N^8$FXgywK~;$gZYJ3a#^2dqeV+=qHex`rq+V_ zz+C2LXK2{3j;RFvV+?5QnduGibrS#r3ke%b?_&dyvzJ(GFI@0Jk zktYq#Nh(r8oajR@-PimsQcuQypMVsyH(u=A6G~zG9&Ry9{=^8`(g@0SKtiCy*bk`b zbQurpxpLu6->`EWK;@$NNXxFwxx|_-F-Ic1_q##RA0j9iD&wJb&>0-{J57@$WM- znkNh+mR48z_=~HouWxW_W`b|N^(x=_)|hB+8D!Yyn=Y(b4ai)sTb8_?zJiAB~m&p=+3;i(QPujLVyySMDsw_ZR z7G5ETKT^ai74Y&|EZ;>a*^4|x5!LD{>uXn8nEx$z?*5893%{XS-5~5Vh{ATv{5{RY z2ql4M30?NI7|UoTb8sRDB^xSjn6JXMo&LrcN*Ij%bfFjO>jU=0gKOe8gekv(#=vxTJ1Sv47WiQ`>KK|Yt^+wZ;2-}?RU^6huuWPE%Stpj%Ablxwn-ei2V%zyePe~0(J z@jB z+9qMxiR<}6uLpp^4k!VRlH^?>N?u4l2D(HV_S&#ih50@6KA~jwpN{k29jpJhu9`1~ zotGKw&bINQFYm!l(+C%0h;Olw(WW0zK^ z)e9}^B&unOiV2}nQe#VSmBf+|X+gcC=|lzqqqShU3TrjpSKvYkllMNAGv&F`)pJq{ zeV%=}7odBZtClz#8`vXV_K{n$tH$A~PhJM+t|u5A+B zBactSiVJXF!=#37t*JcbXZ1^^uY&*(7`*=!Chk8%jQA4>M*Wja6)sWoPoSh7tN*E; z#o$mq(lHhwvHAxWD#I)$KrW;}IS!er5i;cxUM`32d&ndf7iZ`-8oS)S^%HL0`YH4C zzh-6a3-+qXN+p3**9!y!g%lb~m_j6WoO3UQpsgb|8oim@O3;oZjgF=nK$!ApSkmD7 z%YNKjPtxbyi~V=zIB^MqR$y}NSF7;(nn~?##tdaHuw;C+%*!u5$9LX)n?Lx$?=Utp zf+dWWV=kv8>v)_!Im^kJ863w&L^{S+=x*|p;q@dADN^S@c(dcO?iy^RkgkL8+Ki4A zKuX$?=JN9wxOr=VPd~dxv)SYT(i)W36qMlYQ1g+1b8*4AB? z7Cz$BkN*|7ZvL2heTPoaq!V_!ZNa+9wHh3yO-ieynea7p`4B1NCZe%95QGt}K+_70 zeLi2+>~(ZELCH+O2s|YX60YEGjnPftqKy-^y0 zP!^Xio@IS=n_qwMDVx9AId~uqk|SBiePIa{Paqz77H)k4f}-FMclpY~@qV zo@Bgm5y#5)tLj~TA71jRO9L1HvztZ;iF9ndVvbC?h*!+x=Cas+oU9`SVHnbBHCfsC zgyqGLSXus*wbd&uFMme8zD*Pvbe|Bi>)KNSY^ljwnyG9=(bbe)&4{nb*%}!et;xC= zX~X;*4Y<2^aM>CujM_h%F@IAX2Mklk+CWl!e|gu~`fF7K^r^+6GYL4ZO{tJ&YGTYV z{yT5*{0rwP6$|(5!f7s`-ls)9+Jak}NNY-k9M7FU&HL}Y#YdlBq1A2^_6PoxyRXsc zNRt)ts({((~9VXMnK7wULOgg z^5|M%j2{zSF~ky*{_ZNvQQRRiwXZbG9YXocpQNeLOx<<#61FI+sst1mszH{W@U zlc#1G87W~))!(bTOJd-Y?)zyU|Mz7VYQiWoG5y#?NIDuNaT$~9+ox~fIyRHzBfRNeBq!=dOH!!NP39)7Vvwp=7rD&pp|C`TD^Oc+vcRM_5LWOsL&jg6Zu z&VRs_D}O<=v17dYKoEAi)ijgeS zGIwHZ6!7Z}N(jZ4F7(g{7?MjZoMK*IcqU#bg+u@#xIGKGmT zvay3tS;_<&MMN01X*4R_zWobs-~KfV3%_M`^>cQ17U;Aa401(cOWP6(4qPgm7=FPu zt*>p>Kex0%N|KYLfbFK{`gX+aUCmz0cm|4vEE8j8q?FuUT%l2|caP;L;|mBQ1K!za zz-kR{?U=vKHbik=l-8z%vZEB)Y=&>Y^E!X>` z<6dN)W{YaA&R)GvqumA}P*S2S3(K-_9UI?qu^fexGKTl*Zin=Y?{a!>ig(_8nY#-s zZ0}SE!=V!u#zl)lGXuPV5dv!*3xFq@B=B1NnoGWP?Hr@|bBq@+;aEAMef8|ck<3nI zE9{()SIXg!7Vt+(IF5^D*(hb&&@^i`);F)Pyz~(Z^S|NNt)H^Cd7DO1`z zC$@sJ3*$aaXJIC5u=$qgIp+p*dj=s&K?J)U_|;0pLLBc`QZQcd`N8|&KqDg8T;6~0O+NnY8e7}DtZ#1bJ2G@#6B@`OIE&_cn4Ewup5lU_@azfz z8K2wpn2J9c03n3PIAwC~7#X{WC9FPV9?%UTCEpqSFY10Fl)`o#GUXzEIgeY+Vds35 zV)!Ri+r zgWb2)!F<#;Nrdrow?e^oOK@u!ma3X&pm80Gkz$_jed8s5_uFrC{q`a^?=I792XT)- z%}&#l>1qX%wW{&zH{uRE#EdT`c$Q+UT;!z}&vWU*Szfq!mP;4Ua{A;9rF;(CQr%9$ zM`QjWK7q75Mi|)c*2V@4^NZZLahs1mzs|>>USVZ@o2|V%wPq)F!ngIFerm&SEkGcX*0@ z0ne%cuz)d)?LR@co2wx2O;GTs$hc*sh-2_6>Y4&E^P8|#gp?A?v2lDKuUH^AR>mvj zuzVM3TWGE0(%S26?<}ysdX;Ne{{zd*AF*3mqSI-bWS*$U{6aw1GOvA7>PZ0^zyBZAPZgq={c zRo7Hof_g_%@5D)VI?hg10@t=F6>`i>j_}f@bA02i%RG1f45wyim>3slC?Sr5uu21Y>PrZ$JB|Fpe)*9NGFUCQlW&P9cVg%@qI)>^*Z=k z65q1%JcrS8iA(2C^5$#L^Zt9UbMEvkg?t{@c4Aj~I%Im()jxeU$;aNwhmoe)YVzw} z|C+!2i@)URwVSMO?yy^H8AgwK+EW-;7LMDB6GjjO9Tt{XSy|uY>dm`!!VZ7$55C8X zm(DXeK8jM3vC$D;dFc{gT))jHSFY39IW)xYU>btHf$1={%CP`=qDW$3zpr6B0O!E0 zWR8`gn3=n8W_I@LP>pSqpwz1ipQOZXs zU~jL=r(ay-|ME}%TSiAmSY6*@ZG8(V1wm+9gd45h2s=OH;d?Gq6Jxx1={)bf`x;Y| zKdJ}XDf`HZ46+Zaj1OCbX&(yph8s72%*|Urp;}#!`{o(uPeOF& z_>i>)vpLO#4)6Y)2=(0A*(<1V7aXj*6VO{2X0kNYHvG=5{EaMhzLzbYxZgl ze)G{6*p5Rd2x+wqGfAoO90%8R$oL*(Vv&SEP|2+3hTd*|2Oz4I#;7e8csdp@qc*DJv#FzTrv+xe3jZM^!9>6mNB(AxCz`>N{>2!O;D z9cjbt8+BONgIl|>-7?sIlgkhnyTG&sjkHMxZ12?&ve9?mQc8-&JZDax;NrQHJa_RF z=T6NsGd0fC#290vBRGx?I_fX~CGJDZzYmIZ#L~hYKKbo0`SdqGV}0=!4lP0{P#8@q z6lw6a@!DA0SPxMQBT&MCoJ8yoh&1RhVtaRwPp;gc-3i#;tMZRMi%iyIVzk7>X!)Q5 zuxZCel6si~&S9_tA7Fd|$+k}oJ&OV$k16;$O}zRGAyCp~BzF$qE)7lkNw%|Ghs;Qk z>{yA+NCCIt#*tu6yVIajS>x8-UvcBcPgq>|kd5^l)T$eO?XeR^$=YV6AMs$sH6gvc zopSEo>nslVF^o*Y%|aCx_h7kZ;`L$I0hby;sGcZ@n}?=rT?ru&QsQTP&Yqs-JKuVf zciwu5a=Adp^RR6j)rA#44tDP+jj`LWe)ch+eei4MZ(c=)b$lCwP!kBV$_JuVpzP_OXHfu@G4S7R%CD*R>oJ-?kCM**Jv1h9$FlIt1&Svo@k)7Y-$4iy2MdBG zySuC0yz@(b_&5J|R#(2D)2ATkUcnO9PlJPpkr7I#Q6NL`a4K8-L8sZ?w1)$07&zy1;9 zVG3COZI%nXsCIE7nf}fYnd&5b-Q)Z&@ zJUMT?tIBtQyfQ@D4t_C5c6@}=iAj`WA*3XX+H~4A7MDKa#*H6w>-H~MT>g~p?FFMI zc1vv=n5n1CNgVJz)s9*1?^NK zkH^m6cLB`bM!m+be)R9T@#zQDD_aKf2uRx~E}al$7&j(Or6!BOI9HoNj8_o7NJ^8pp3a8Gk{D|XdNq0!Jj5$XX844v8x!c^0CT4GD@ag1Un+4L#N%Ovb)0a(#QPf zH~)gQ^{Z4WYc!h`bQHUv5?f!G6@E4c(-{-eD>#Pvy9tAOze6uwQfm;_8}C9GnRZ%t zDzFl(`_+yh5)Q%{AzPSca{45%ymXGS@ev$11HvMVA{G`FXjB%d?QODKsj#@XNM$?G z2+R?}FeD5^Hnw*7=$kwh2cjJ{7Zk8+hO5&JI^@+ySi_ zFPF$=J>-TMfYKsCg0>WU>BnW+z$}qE2U>m0E}R~vm?a3r=)}L zj8M$X;8<>wD2tR5o=hE&}sqC(@y?vLJ<la+R{zDp#-EP8; zp%@w#$DyaQa5`_8KX02ADA6+p`hfy5^C#}4ls_Qh|4^G1tV{?N_r9!jW zj4=wDW~;^e<~G0j?Pq*;5#djUF^|&7q!KD3Qr)uI8K+|lsSzg;P=Nd*>N)3&?2m_12wM>1| zFzTWm987)5S251O3Qv(K@T>`dB1Q{{n|x|4<&$woDEd=KVHulP3Y2T(6$=z5r|=5} zq@`%L>QwetxOwZR#=F1pA*-ubsn@p-tV`#_YX8}Saq?vpec5@#C}IACYU~A(Dd4fy zG>*QjJEo9lprx_v?L2rBXuAZ;2Vi}DmukI3C(vZG8Oo(B7tTzO&$>t*v0JHd`s6HE zuU_Hv&p+qdwd<^`u2S3GiHnMAf*@pmX_eo8a+SFglT1&H4v#}V2C7$gSzEkKb!W4i zpOH9IEfw><7U)C|2W(4`5f+ZHjf!u1v_nO`8B(oP5seNl9T5=`MkcX89;VB^T0o^% zV`XCtB~`5Y?>}da5$c|eNF1?oL8Sy2@G{0Hu*R_f_)17%^8GEkH7&HZl!I$$g=^=K zN+B$X<+*sJJo)JfGGiqg^$ME{cUZdf30Lm?8?N8_YwGnaI-S;m<+i~vdftXp1vpnQ zj=wZKci#llj6Y=ZU}F1sc44Iot94j!3K{_>?a<1BI|lxg5d^|wXRpbJpWWue&u*g} zA3s|pmoJdZdVK4RGyKjsp5xV*&T#JhMb4i)!`9Xockj;g(Z`?i!@v0{|NZ~>pV4SE z2*c3S4{S8~cYpIMPM(=4(*;-@y&K25?YF`0TO%EhxRD4M! z1;vRnrSU1sQzs~opTKopgtX8onyoe~YwLV*^(Kp}+bpeZv$j>C9R!4-?q+goOslpi zjHv8Ykq9g`93Ym|GO?v8o4(hEAT-UIwb24z!QcW`c#6t~Jxc;W1KSvUb`=;$2m;zv z+N&(rzF=(X9HS>sVS6rGXll(JZhib??#};)#f6VqTf55c-V&X5ov5#+)^PGID8*qu zHKZzdUo2nHE$26g^ot_n-LJF-cXl;5cg%|4j06F3M^0x1oKdh!fDc*{36~BwL1dvM zHYF3{+>)VDSN;xF#-%TEKIzp%~gFHQ45`oTHIN5&|Z%gmiR!==k_<2nw1^TYqZ z{QObu))uP?B*wrCzw9PBJ=ulJhUU%J|G2rO|P+*&^AZg+)Cgb@ajjtF%~&hz7PZT)zF1Y#_e=_b6k2MrBDgwPs=F*{$ShYqaY=au+M z=rMYCz7ldVslR7{>7*)WL?j?$taJvakf9muvAJ`TTX%lK=U@B5U;IWtG8JVrK~$M-XM zeg@z7@qM3cHcP2gWPEIt$;ol1rzR-o^K5MI(rC75cRHr~WENpy0 z+uNfZRN36V&C>Emtgd}dv$faf;1k3xqvIkO6LEshUI#Xt^p}zzmdvIHR{oUQuMvbA zI+01;U8=%HU9i~{>@;Dg9nnK6$2Pw^26h>wN3YP80wKxx9@A5!{NTH<^5)Cu867EN zTNa&8$jKA)T)SQ6)_j$v)f#{P!`n)8A?6ADLNv+zTTrQ%cduu}vL#LlNxn47-1!$c{roG;UAoM~ z+*vY(0#eI?^&>blNeCY6edaNiH`<6YXYExsn_;7CRt+y5JA+Y(pcrw zJO2ZV+f!8QYg8)BG@F$n1wqVB_EUv9&gPB!w^WB#1oa?YeP_STpD_PEC@+EtNh1{O zv=plii>n(UwT`LLANCc+6d)~Q_d63t5FicSRak&Q@l8&Y`0jfz@elv_o6Jmi9CPw9Is&zz(`wObG-$V4eb-A$lfgNA?h=>3@jj=Xf0^?19MZO9>7eiJ zZH+IcZQGQ~C0=Wk$k3=j*U&n;r+qJkQR~mg&OJ{V z&%BBgaJHJJ&2^(=LUWr<69P)6MjHT12qwxt<0X&h z&W!TLE3??8oBZ->1v|Hi<-2_6^)Y-$apBx41d>i5`26Evv$SxRW~<5W-X2?9n-H8! z)lSenDrBJ6ny}djY45LZb~Q zO0c$v*lie1Aig+7-~~+A>7Qfj20qcy;Ac$$fC&MPVKM>JaJ~4s?Pq?=vdCsUqR7O; zJDtGb?L)KLCy$UNduhA{8@6KE-Uwm6VUT+HxH@m7&Ax#)skyry*lcK48=9T4MKh2D zktXZ|2c`N>2>~i=YjulUk1zS#hzWNfU=?93>mB@^|OL&6I}2yz*VbF(@A&if}= zuLNAZQ{&HnvdU=D;qB*22q73BEAi^(i@g2j>-_n~N?dwO6LbR8FX1tT19e)JqMLSs zW!aQQr?~L)yG+cU!}T)p93GCJgb>V3P4L$1FR`(;&A!%? z8!5A~xkIznzkqjf9Rg-brW?{m73#*Fkf{453>x(eCL-`eBLkl`0U$9qV2Umlpr5qX zNGY%_3*U1&IXA=f#2C$1i}}S>?#wR{MiI3Dc3M#B^f3IMf|6r|z;0Z;w;h@s{5yNb zTyT`pPWL*7`Kui>&Q=q4+nP`s5Rz-#WIT&{5D5Vm1_s{qOap<*whc?oH- z{)Ix0k#e3~#<}N3u`Nj{@9@?OWv(vP`RONH+*+#hi_dm={d^Hi3Ub*DC+B8(>y6j> z<*$B@j*Or*jXB|QE@Xo^DWxomW0TCDf01*S-yu^d4Xxhvphqj03S7E)mQEOA=Uk>H z$9d_}1y0S)aQ((zJlE;gbsRQV1mrv@XH6rOjaoOyN@L*YNs`bndBPKfo<#vr#s~m^ z=m{&OecxrIl;_0cxcK%r-{$P8S+;j~`SpjNvAwg)?p}>rCt|$`D-GidkQfsnXB)uJ zVl7_fBNG$8vSS$Cvy8uBC$6bi?HCoY8JLhj&T}ahvWyn9OqMfTU)>?-v<)2B|cv9XRo6NW)j8|<;%FjL3B=P%KKlgToD<~hz@ev8=)FCmqBbO}is zJkMcfdV*Xo!_A* zr@x`zXtLFU>$@=HnJ}OwA*gz=keU-%e?*en|$)ghxon)qLZXeJR(Z) z&mhoPwu5b3ShmEn&4n77ImP+czRB5_-^MTGA?&Z7e00+L_lt`eIF4d$yv*3tsKNFE z%}$4@sZsJ-7uPn;RYRT1BGkq-+-@2HFjaufGR$vx1weud9LJOnp9OC51cwBlH35*r zto~E+xpg01DMggaW`gg3_ucT1e(=8a_8TuVF+NJhF`$o$@ey8q={f%3``_a8YqwZh z*`U$xuuy~FY{Cmg81;;bpS9p(39LO68Q6`bf6%MN(XHB}gejptI#T8jzyEFi_8)wY zi|0>~&-!fd?$P{zHVDHYR{vvxf1f0;-yr{N5Efe6faLBq%q>2$d8>bsn{@FKa9(Vn;eky27bAc9Dk66Q)FrNp)^l$1mLDLMuN1#!u-oM(i< zY!TM?Ov}S~rXHp$)fr4L!P;XuiAO?*Ju*)ljbkza#|FOFj&18#UcA5`{osA;t=BKh z6VsC-m-S5-83axoDth^a3w-nKS2#I4h2z+CB3P`$LLIi-<^fOwrhPb(HU0o60UpCp zChEyFgtgOxs#`un3^G%FAdLAeqidaz=YN~ zUIs6l$Is>|jZbmn+$Hkm(YUeaW2RJPRnculevhg5g(*$-=DdfNrYCE%vxNykUjPa` z9?|(o=vmSL1jhE7#`pn}@3iaKOpJ~2{GqaCPOi6;aCSh zZw5Cb&{7aug3VS$E9|McN=hci$2mPaOS|e(%;z7m{Xf89QsDR*3ZoNDojQ+|$ulu~ z1}~F~yET8gWLXNOBvMMG6tMy_kfR;HBt`%feABxj(&$hppl`C6rnE^r`qr0xDn~ot zuX+NHkR&D*cmk874Y2?yO;;DP0(A^>rf9}x=#zr*6{Nwq+cGp-!0~10hQ#O;@GS9As>C5FsMdfLLI7 z>l|iL%J75=fTukdAYD!)3w#d)!Z?|nn`J5S{Bvi;-~RpY$lv?!J7jGOsiR)0oq@qD z+on*+F*h^G-0T#t=W_klU0R)hN?WtlGV%SQYqWukWg2-X2~)nQ^FQvHZ5nYgiXhYk zK}6Q~xODC$g?x@Mj96S+;m`l-r&McoIzeEV+%7`)jKTj~8IygYECUQu9#~m$OW>8k z8zHoF)Iy7oZ&z8W2h=6F8H=nVaa6CGj|N(y=ITy||KVYXDNwk)`z>2rJJbWXUNMz@C;VP% z?+eA=9dE|v_cmL}aGG||=Gv`0EG({YVrJ4*+P&xLBuNCyFuwBR@L&d<0@yjQERYHv zY3hxTpWWPJ-4%RT3wUwdw+~gN)4enMt$YEDQYn0{SWb#Pe zMn+#oW^3HmTgJYKB$SfweDY<{eI7~(BLv3t2z!k*SRp|MPGih~Cz76a0gx`zn8i3; zy33Uil3X@x`oE2h;5d#c2X=%JZOdYOw9HE{J;#eLT;!9_uCu#aC5$2}?TE#iVSL{* zPQZel^r8nlFxUa#GKoWxh9HPoT3u&hWu52u>Xb@(g2eD1j^jH8x@GMI?e z7{(9K;R&OsT>vB`Fc&+ADb64Oq?9_J&r>RuL@t}fvTa1CbMJ>L>KV_j<1#VjF*7qo zHk-q?Z91I}L0~{a6F!Va##fhAo=sMSf@8{pH{xQJQ53PYv&XHwD|&5X2iNlm5m=TI z{V-c?o+#S`Cl4YEmXB~faDBAxfwU2}L}nB!uaHF>k+)F3qV7rN+W~i5F28wz002$# zNkl#Xu@c)+Va69h&wDhU@1g9Oh&`T_@pK=3)Ie6eTswhtOx)w0E`^nMxY?26qDnl z%$=BGd~5`%ER)fZ?!GZdNu6l`DG<^^*|v!fD@73J0w<2Y#Ceur#1jE9>OLUzafUz$!}ykMZ!RC+s~alEyl-2Mjg{^G?q32 z!U6IYqU<6a7nv2vqL0km$ecua0_n!g&;P%@_jqYg zy1cpGr}^pOWL{HU)dn*_HOxUI>h`^P^Je}sf9H41_k7ROETSs>4-%HaY{p?WZ4>Wr zN2~+z+o1yZY~jQX8-~YLG4|7b0j*{Sx8vb^-oOIUJD#la5f4Q4sci~dP{Qd}ghtP0 z99jJ*kw@W~st)`-D1b;F$`~m$O8Y475qK`;VuAA30~(bQ%|?}WvyO@wXqA9JEZZWL z9U+w)BALr$r<2fWLEwhULT-7?lmME2;Q8t=%E>#DC2)+8IYvRd?XteLrAs=Td zY2kE>2Xv?a!dR6$pa4Dt2EhQ|;$f^T-5n2P7?eV5Xf#@MIxe1fI47h+?q|QN1%#Z8 zVhPyh08>x`S^cNz!PB3ak^H z757MFa%4xx$xTd#7C>kghYv;4lF5H4MkYi7W*o@bLKR-?JuIrz@v^mAD_L(e?dLC@ zGJf;kIGfvhbV^n5nyBVFhEc#A^021jm}#rG823MQA8UXVwxJk_Tf96PXUH+J2C9dm zf`*XD@OMZ6@@Yvwufkz4qL3=`p>foN7J%ysOL7n#`durGi}rur2;G2G3>>?w0HWfc zA$khH{^s>}_|7+;XLfp=rR6nB<*Kk1 zH7jUWfMY6d_MsYnd-1hqGHLS4Se(DNkijwgImuXgooDpvBy4zUg#82R)f&xa z3)l6e*1@((B;&;5F)Yh`DAK2t5tb-F9sI~p0twE>1uRtU#Q{tNn=*-Hs9H~T;7?dS z4+=md6Tg78X6iyNeLtYyXi=$Dsh7)GDv%YwFM{__>8uv+KCMCfKH6>5YShIf?91Cn zm43z%P_>YJh?FIup|lfb0|*E_m;FMKx8A?O4}b6#e*b%4$1n_9?GEc(MRcQtwR4Nu z?iA+SD1I!<$KjWZcs%+oldNNMc{IT{CzDJk4}g)hhM*J10G=sV^>8^jNwr36h1>Bd z*DBn&c8ia0-eqNNlX6G~noXx!I5ES8vkN?T{xnllV_gN{`9Ae}gKDjg=lU4RKx^IQ zYBCHl@1yR{hVF|6sBYsB681w$QwvE0CV=X34_m){9uxq`SeKDBOEdg8zSis&%W8dn zi|x%V;+Axo)dT$9gAbAJGv^Jhu>+T8wJK&|pEYcSH{39y;~g#uQ`~@|7}Q)%%lDPm z(5N@Kzr4ow&ORsSr|Ep{MM~u=KmNtrw7e#2=QcdZ!R!nyyzsFHrd!LRgeUOQP>k

4J@Vm@Rjgv|y=y)FGN`+FXh8rpXzfTdGAxpGl_PWmm(KDgt znMm4@a%5kxr?cZo9AE_4=NTdh`8+9rDuTDJBhS5|HSFw_Sb4C?=H?b>=EepCXh$5V z_g7=9fObPF_JY3E)DT*}$7YQjLqI{Jap4Z^cmckKPP@hS?k+cPEiyMX#^pN-Jr9dGR#te%3oi-G}o+N!+1K$VGNG-2}HsU+W;Oma5wbUWzE%J0eK_nUkM z*sOaJVfa0l#g%nFyn2WCuH59^E7!SwZ-rW|PRDio=+H#V&s6=* z=S=}@B36J;=sE8d%B*kiu~#T_c7B|X)$IBaxS=kU)e(BdR6-Uyk$}Yt-R6{@?@_6g zc;l@Pxp3|zUwQE&fAGDpb7OIpfA$we%H=BF{vP(?6=JXc2|AtoXbj+ubelum0`N`1xyZasAd3wR+>>AYNFk;rbqX z#S;64GV0I%4SwMB```Zt<0B(%Z0+N@{*hx~D#(Xqb6x<|wsZ+wTv1S{#V?NG#77xC5wK+0B}&)NL(Y?A-+a*i|k7>Q{4 zLWAExx605eqB~WTerBxGN8A|Ex`Ea-TW!`hcKBcZ*Z+eb|NM3C-hV)?UO$@ly`PW7 zP}FO6N~H=xAhDsJ|Kiu&y1PuL<97Z3C0dm zx*$aRBm#@Rk5JY>Gl9U*lL7!Su}&ijAWc*hWC+mb`ST|k$tMYdfOf}46hLs~imEa2 zwZYBhZLTkFP^mSART!8|N-aRjKEyHzpS`H4En4-P9_+Nj+JJCZFYp-~9b$ZJgo*J{ zCPs&d+cuU`xE+^rwT`Y9m{nC$wt=6_Q8Lrv!)Xv0mT&|r6F+3RQd*OD6qoWg-xzcF z!O|#XaSS)e--ZE>OJ9Io(rhXSG3c0v61~m=1vJR_7 zVFYe;c#tBvOe62LVS`D~ie;T)4=LrjbfVcQl8MG)w&dk_SgcDqBp(PY0+VrRENsUlUM zeVrOG!-~JS1*Ik|?F;0$Rq}&==zd>F% zNp;Fh*gltr9lkr0;2+F6e1F2`xgndWlttW966yEq;5B6PoA|zq4#GzFN=Y-*vAPLa9Qf);P2)kQt@`eGQvs@%^8&U80H91+3B|2sw81>hq=mI`p^!UI_mx z0N?lZ?%qD9XNJ}6%p_yuW7O&m{7#GhDBmEKSR91gE4$oY*%k#5PULfW;RcNL2j>sg z1N1FeieCP>=}SDI6IucFMiZ?el}zx}SDqstkCD!#7?~XB77L~>(`jCEOD;2#zk+FGmRB~e@yeIXS4kAnITT4grgu~YAh8=?H51aaXJ5JS*KhEbKm8?de{dtjb{HsSkV~g{<+;=R zPygMY@*n=;xA}u_zrr_PzQlK4eU6c#9G>SAu(i^R!|N8jX)w1G}u&-k4cfxo+F)u zd|w{E-Q5D$Z`@;ZV~10x<}j2Zt~JxPW^5|M`QLk)>MN%x7s?d&OB72bOhaMY7BR;r zm(MdfImz&_ChhH_yr|(u->RuKZXo&nMx)L8<`)0-U;bMv)jE^oBfNO&Jg>fZo>VGH zGLgWx?H;*dbX`%|74!k(pTPKyL>usksLMl52p%fKRA3rxZ0&LV_I>WJZW16qc+9bw z9v$YTi>LU9fBbE}`_&hS#~lnq2oZ9b7(e{Z3ycgWnVBBuXK#MUtvhRv>=W5Njc^!r zKY~~(VsV>PDo)I{&;hhsZ5oYs7-Kqg1fB!F5k9A{am=xR&!YlpAlmhP;5!Hm(*p1U zt?sYy1n=KiVq$!hSD!zlys0@#`#ZFnbwU2YfF+;_&;$?N#v>n@BcAOjAMY5FImp;h zZ1)f$)mok92OC_!waDzu1eR%r<&7HKQpEBZhLQ=~kr7(6Et;(spuT)yxVN;zYj3{IzyHOXwAvkpavAO}uJYi)ItwRen3^18bSO_YnIfA` z$s{`L`Uct`KZ!-F9`o6wh(|=(Xg^?WbDNE=T}qYO!2K8>$@9Yb1zx>;j%+H~Q^rbT z0;3~Yo#@GI&*JJ9 z_f|F!YcH;mx`2zoW#9?|3_3RXyeR;M%&C3gU4)SFG=1Fc#d1x3aQ&XnrBh6f4lzAG z4H~ND0_|1hy9>_!p{Q2CZmp!iaL#6KB(F5u(i9-2Or(!YhQT*+qQA+ z9vzoy7??4Km=hzNP9O1$QK8vkXQV+HG#V}LEv@jzTkrF;zkQQCi!1m+z`AX-wz19T z<~HZgoM3i(lG&+oW+x|@o0=e*Oc0MdqF_u@T4*UHWW}K=tT@CRK6LmFFd!XzwmqC} zuPLZP>j1Cgv%Ip-UZF(0-DBCZOq0p+AJIq_R26`#V&N zyR+^VbF11cK3_aD>asuACQR085IARZ8vAfzmqNh+k5O6N_cIL zzxSiRLoT1iG{NmOsh3LJU0h^yXNP*TS9Ui&HOiS&GtAFU2wuSV57vgF4CyzJOK17| zim5S zldNv<^P7+EQm#lH$$=*#j%2G&A%XZ~;|Tb?DS(4=7YVdv5v`l=nu9u^<9fXIn-8ee z8eG0~hUd?o;Dv=zCdWspr5e<$b?W6R)p|2T4n7p;d!$5FV%>k!;|_ETDG~v$DO8W!l)LgKas)Vm9eaf?O_1G8Gff zK|_%r&XUVzNT-vc0Ale$1>iQiGcNFbs^tn>+q>-Tmub}N#7xDd^QXCZ;S8x{EUfq& z+(0_|OpgsSKAa;FcMhG!y^?Q*X&S^Fo0*9bhH@FU_lh*zN0au(V~*5pTv*`3=~-zZ zZW&mnfoYmuIJkjW2`8q<1$S_Gw#24KV;_c=k4HYw3P2&2zy`1e6cHr=M8WHT2OB%I zS{*jFc3ECoqx|9-rpEHbV>Z6)QLZ;x-P)(sd6>uHaY{fx;@`F*8G($`BNx?L)9!S* zeru8EFP-Jo!VLLrDy+}{xTB!xbX@lKi@f#0HI`P_sMH#O!c-=gr=I8A3*Y4V$>$hK zNdNLJ=Q841c9cb+cb5PhTmYbxP|8jxV}%P-J)J^v0o?w9$Nc? zqEnDa$4I2&#A7i~26imb3n-$&ZFfgT>j1ajq+G7hXtp8n8A`{gR%`fP{~Xc17(sX+ zOv50TNs~z>h}n;StPMk9SthfSV~h-Ch{qh7E%(TMCr0vIIy2A9mrgT2l9TpfQS76C zk}HW5q?7RwRCVZ6Vj!TkG(8%-FD-!Ik*M&x;`G>9O{7dXq`K+d71#CH*xX}pzs&vR zb+*?xIW;@MiP>?IafiL#0(Vz;XtX*5?so(_c_?mukTEj+_rb@&G9(j_bHoj3g**7Z z$KuKZ?%iMI@})B@%uV%t>W@*Rqcv1&4VG5d`QW3Q6p9sG&%-fovat-mclvw0G<})5 zp*doP196v$)Cd>cGgR9Zimf7>)or263Yt{>DrL9A{oNJnZj+YZrW15PDQeX!d;5D7 zOZ&7tEj&D`dzwHIA#^Zfag06-Kt=Rp;e!@sDE$&9$A=lpi2~4vq~(n;)p}}vikB~*=JNTIB;s+55MHiALYdxpYkWVT z*=iH``tUeJcxVog*dB*`-V{JIs3S<4pVSkYjD8Bg{9MR+1@Gg)BQ4W$0DDN z6LV}@oq)~#8ahh$hZEoJgMdb&!hC;JaXgSqh~{w6d(SZ?7`W3EP>`=Rp7tqKE8JgQ zV`*iL7cQPAlTHri+@DYp1R7jFU}bHGzkTBauHIOp)#zYogOTJAug<*4AD#XI!|4&> zQV)a(#IXzQaZapF7*g|7o#-=*!fsV>xL`)yi&i?-h;&;f3%&F)PYOUGftAa^w~;=97ZLPT*lYU`ZL0@g zdVWB?<1%_AvS|6(a3{K5Warwe2 z<|fCmEep#syOUoH?5%0FT?)kt%j?^??veNcA1Pm7N7B~ErUX7O3cx^C{ci)`N03%$ zkl>%mBeKz&un&i25O*Y++wKH(Tp!ohN8P{yc+HUxTr`(@cIUu;S_>#>G!Eqs)chzT zpxNCoaPR&KckZol_SC#oG(OFUCXUFt25Ld7KrdfuEyf_8q;}S=#W4gLT!@J!&<>5lO0~{| z^(}7RS>%U5_y&$+_x;Wh#Rmt(_kGIc3dK^HdZUHbnrtl1sgXG@jGbmA751z=%4F70 z@aBLh9h>yuC7%1n*4aq187CH2cLe z`^5^iMw8Kz>;Ru%?{s_0MR$vzLSBUf=VqBE*;I-b&YfgpG*2?&i0^;kjH7p4&u3$& z$jzlSRyTL?eg7z!aR;aXJCD7IpRjx$6hI6?RQ(Y61Edl#&LL1fg#_O$C0u>OF*q}9 zGBT84VLr#{Q$ys2law1CtJ`%}cN(niH+l1Ffo9vs_jNZ0(D2Z0ALZ-AiADQagrb^& z!*=Vh57@Gx98#LP9hbemA~$X?(P%cYYy-ov`pk)gM>=@5nohGpVQ-h>{yyz?n;-}n zN#!^>GRw))6AY#DhuhOfiVg!D>gfFqg=JV|bHgm0I8SkFgLG za&ita$6;!8m{aqUTs4f};Mzkc@$?_ase zt1mxCKASn@M~;}W1{4bgKD>I1t2ggaELAWJn8=JWpPywSGmbJX@IFo)>IkLLOSGeu zVtV#8g`G_rwK9!bIo$Da-G&CIOh1LA3e9Zmh$w%((PF<;Wv^7h^~B;Z4-~w1yD3G_ zgl&?`q{yU`IJQkIqGMNzk$i@)zIdJ&FD{Tu#IQ`&=a%$Ve--+6tLuAwaP1x++_*=h z^+?j65<*{Be#(LWoy&3delm~=90lk;j*J7Z06zj|!eb3Qou-n>H6DlO^6>HqT+B&5 zf5HOS3uv@Fnr&Zt=7naaV<|>*4$q&?@|BBurbgn#9rL#~*rErh-)(RV=^`|d1jp>H zFhL-Jt)1Nh9rr0ZODhE-rM2$|_U0TMLS zEPWMwPaz559HIc8N22`$*JCL`+s&omTn^4=W%bX;q^*~!0MDniS7U#-O0C-JL(eK4 z%OIDqIX|0Wb}UIYWeKe~(DqSY@LNE*bZ@l>IiGykQan-)eh|=XwprcSrrv1b`F>xa z(xd$7aO;O6N2Yb4dF&xR_B9QrDh;K`4~;Q7eS(SU1#BnYSJ-oe7^=HDnHVEyM#xNO zF&!(61Ay&VOizxGNhbz!{vFq2w@~51);`rn`%tbY74k5uFw0?@CfRg?Y&t{h#t$x zcfX9Rm`)0%bC=6sf#rLYb}DSI71`UV zbO|DWusq#hel*4ONStia#xSI5NYjhjejjnB`tcXgZ|+CHpsdr2DfoUsz1il@(keUq zB>^J^{S|))ex#p$q|aP7O%*~_HGbd|_<=Mz`6R+m{YoGRNG8&Zj7~E-bCN_d9j>I0 zIDgXs%fyV^q$V;?i35P_&k@Hh9lut7^J<8nln3QH8VKr%><9S@Yv&=`gme|}a999P0FZ;;k zf50=)AK*9@Ko*(&e~6UqW}f2vCU6P(Izq6JjE)=O?8g)E@+iDC0;jXGx<;#a|K=D0 zt!k59&kNO{0>wx=MmlEWSOWh9;ezXgM37H);eF^q)i+nHuB)QEY{Eeh z&}_AM>-}q7yS2pbeklUYa^xh6BnU7KgILVMajfpc>vkwsY+!iteDN|lhce0=WzPm0F7p2r@*_{ z@A1}0cj&kt0eVmYXg#n6T9eDAm>9`1oKFd^!GLr+!B9R;G7&>*)rIvAeA;KT2L-yxHXbN7#j);dHT2!l(hG%)tjtu?Gb3YaNS4U3)?iYZ41k?Ab?yp#icWIym0OW zxpY!ez=!B6WhnMa6@K&nP2PC-I`u{$z1-n^=nH1z`ndgT*Zrn-mak7%BnEsx#Z8h37NQ!X{| zIxZl+1YR}e6d1pj6bYv2-M1-y(Xhr`ax zKv-4JAZfpOdZxel6k#NNDkTUz)i?QrzD*zaw{Xe@RH_~BFYd9kRifEw$z)RC#0+L9 zl1z`r$)qG!>Fbc)H>?SGT!uT6?i10E3Jl*gl(Z7h*$_8+AUoId*xK1=XSYDT(U2Yc zdg2}aLji`R7#_|sF+R*tE=|m_X}E1xO6%O+TcXq~@^Dn*cPwGx&a^CY`B9vB0>kW6 z07lqg(KN7<4&&!XIsM&}Og%S6W-Nmd`S^X49~`pTAr|ImnVX&%a0AfLXtcO>Zt9L0^ z8?-y_VLb_S_z1RuTgd9a&ok10?bB8O(NmU3%=lr%q(6o5_x%W&;1P*Ozu1Vv_zGeH z#$f@;mXzx9+Wl&{igf2-=z|W4reF~+ay0UN-UP_SpZQb zjcH)UZPJq&&j0W%XMb>p;rSsf+X|zshD?0rfQkV&x%@EaPM_q=$$65=_`u+H+8!HQ z`~1xtAF#NxNxj)Z9R+!gVgrVh6q^c$GigTh88WH(;OgJ~j-u9R@!^dnUVG~r?_Iq| zz1fnO!^3q50kZm65r(Q|#1(iZ+^rAJ@7O!zy% zPmq29#~%HUhWy^Mjf6e?+>E)L1c ze~_pDFrZ#!TK6x&{dnO{C&2eL$}q_0TVgeHjjn1LBWcEjW711Ec!^v4lY?{zmB5NHD5r(UbF zc<&<`jVf4>7)mnr!Zc_8;0)*g=o|~*S|Bx?7Qm1)B}i%5U|66`0c%b~T))qUHe4hEkvZ7J4LSDUg3>*Z}9UsKIAWc`fJ|)=nh+tPy6}63h+AcQ^XQ@Cd_|0 z9w=}Ep+?TrH%G$HqFVk}!@mniwqOOR^1Bp%*8sGsAZ4S*64HG)9=2tER9{GgLCe|e z+c$y%`@Uvxzd?MtNH!NIm2ya>9VUhxF3hEwA5UA?Hrd@s$@Z2`!ar%7lq7y8q&}325SWg6W9~ ze)NNHGBrBJ(*1R|w)Sb1+mwS6MZ17XcqHsNjz5Os8@&@fIG7I=G^mF!s{5Htd~E~- zriYn@_*jgg)DW?}!^o*&rY_Ggd}fr~T#nR8O5n*|MPh_jjja5>>rrde*x26X-~8k? zUVrODwY;{4@B4?IK(p0hX?2Uc%Nx9W=@bda#2f`vR@=q(d@`9N>0}bej$tU1 zj_b0xy1}pBy~&NctAa7uEwHguq*STV={yplspzftX(YzAf)oLjk(z*GmE%wV5fxI3 zFivM0Xay?JC|`PSjv>Lo86+mKjG+DI2?0Y2RKk)Xn%R^v>zGdv(KxD+OSjT5d{L{r zY;9DDJ0@eJX{P3~Wa0)3lL<~vCAqy?!}T;RPwM!ie&i4R-eY0{{gy|RO^AoR1Cn9J zFVGSQ+}zsXy$`N2K9VQq*v!vNGBlJYkq8lX4=DkK))Lu{J8@pPc#fIran{$j*;wDD zv|nXtFNHZ3;OES_jPe6)7awuef%#PR`?uZ-atS+Y|Z zCZC^RF~kLU4j8fx{G5#+H_-|VPj??D z3o-Slk(S>Ri1naA{QS{4%x7RBBc}M{B#cK5mXRK@ZxDdj38+^@Nyig5$)t@k4Q?)1 zxw2Tr4+3mMY6|94a!yC4oTCNxz~_hQEK^BfvF^csTU-JVXmGvO;O^oIn_IiYVlnc$ zESYo?Gcs@Wpv@jtswril43lJ9=)O;!nC9%+6U;2kGB%$lJDwtycd(NRWofWULpZv|6{PTbPGwv*{s#XNK)f`#d&}g+W!$zSmUphrR9vj%# z4}21FhkQ20#AuF8GQrE2PBS)?V{5O(zy0gC`6qw&SKL}!qfn~R?zm4o`J+~KGvPCT z8L?tJ$ijB4t-_DP0x;-v!xb3Aanw{2BWxVX@n65rJ6CRT>HKNF^YxeclRx?{)05+*lL>6o zAPC7Q)WL4QzEEMAF_W01(6%7mB;clDtO>dmX0Ek7o__DUJ*z;25r7%UUzEJI!cA$3 ztcQ$2D9LlCjcjOwhnhGzFkpTC|GXfe(P;3|wLARf&tK#1_pfpL-YUC$B~;kzEUIw` z2p+NkWFJ`HD)P&>Zt%4i&v9m9mP|TH%(k&i3GSs+2`-(R=lrP|+AW`Qwa!modxxL= z@;zRE|0d;1Bh391vL_k%et>OTWYb9|M~9f79>+2j9oM5;Z?ID+v$<2C(drnU=SLUm zAVKgyrbkovPY_q&SXzV~hXRPwB0ggFn_hr66>2Ds%EhJWW}zds@zt=Kt`76$F+<8` zPlkm(8LO8LfWb;T%^+`|<^Vg@Au?bgggM~A57^qS)9QF6SBexGK92UWl*SDN6jXL$ zOoye7N2DJjn(!m_Kp1oL6*yMdLd?``m9SXtZT_We~}zI>jG zXBRj%Kh5Omh;;E$#-N`O^o^DR<+z6g`gG4sPV^Dh^p_zh-P^DGUDcO~2#<5%*u7qb z(T`b!zimpBlD*vm_aAKV(apR3^3C_Sdh-rj+xyh2P3?JJqyWORJ-n{LGKIp-b76UH zkAM2-zvRT+1e4>#%uI~1Ff+#ZSdMI13hXI^YNf@Mn@ha@-c3Hdu|&C2$MbxYQrMPB z%(ieGn~AX@PR>qp;nXY_PtS4w)C{I+(r$MsR%)zo?Q!+ain@AdndJxD>=i3?di?=X zNb38CNMLY8>>WOlBRFPe@aPo+Aj9diE zkQhM5?#)FVCcT2tN%ER)n$0$Qg(8KL5Q$zmyTEhjPV&OVvz(ZlBAZE*OeAn3j`}EI z7`Y0@!G-6*1h)FFXyC&7O`1O3a{rD|hHgL!90V{46_+3$eG0`g%c~pQSzO`jt$TcM z?KW3$+@Vme;C9@u64H90CF_705h3+#+r~01{6Mo`tntQsx4FK!&e%wnnaNSko|xp7 z7fy3-VVcq5EDA-v+2Z}{i`=-o%I0pd+tfpAjZzkwRFZS2X877G7dd}&mb3HIEX<5C zJvJmAmOM{djhAbjJvGPa`Dyk3^?Q7HbBVk6*LB!m!b04D7l4-#a*!s+Uk-aKd_l#&KoofrO!J@X(1Kn6L-WQun`qAn!Rqc?wr8so+6w6A>@7>M{Yoo>K+|e_Z0?p= z-z(s(+GNwKymu{2vD(1*11=f!WYY->+L z97B2VaRdQ1lH;F8xce*vS?#hhdEleSiKztNpRy(0m$bz~h*tfBk6*v}AN|nND2T&? zgaaRj$Tc%eH91!^*Ltlh@fTlnDYIJ`;+G(ILM5>ht`kfB*aZ7yr=@nV+2`6>~7PM*IH5C_^pL>|`R& z?BpotPtLNlUt)b*6iev7SP0JL7D5v8aASf`SUznFKp~oY8(~Ck1F?1hr4H;iVay5h z>EYfHWcM&REST3%A@#ek4hAS&wMO{?MgSQ@leeL0iy~#igy9et>`MWY?mMM^Oq0B)!zyE6D>v|1eXzyWR)P0oH%TYs{N(4aGdDNI$rCf2 zn44yLVvG|r<76{wGU+6#WP+IEpaTi1DHMijVko(qAkfiygwhYLmPov*%r4Y7(18Ra z1C1X9lnN#4wI;QCi=Dj!cNSOp@cJ#iy<1>?YnP3!T?)lY;0c+g(r6VbkTP%|%^lz- z@DV}*w?K~q4#&1hCE}!039{)VQ{%(fwi5>K4AZfvXTcUCD@x-S1D5)4fJ4j$L1mg8^%G~xp6AQ~_0Yr)mmLtMStUo!rb(9}=d zB?xbJ6pP5(q51E_Zc8R?)|NQNz=|5B$Pc*FQ62L`cveDaq$vo;c6}gW9F1W;{7Qzt z7U*)Cnoh@~)A49Dn^dYbij^uWn_FDFb&rvuJmX`-%ukImGL&ODpJiw$$Mobl$wZt~ zGD#v46E+;vl&(9m7^dYMAVKM=Xr&bGc87MyrPX%PI-pvuQ>!;9S8Ck4yG)@_p;W1| zyH{joeT$U`>ol4z8qF5XW*bjHJsuJiDhdD7b z&iPX_oIN=!)ra8&75D+Wdj;;TY;k90llyBs?C+M?-Ys$^km8=Lz3vJvWeTR^^!y~> z`pN}fytu&la86vjC+R&9B{Y;lGU0IP%p42TW2~<4^eX@xnTN+xn0H*Hpe2M6WdoQD zG^!Jb0%&>?=~quV@jq>`LIsd_gwDI_%HLuKMq=W>A0~&zEe^8;x-mK>S&^NGLquDM z>DWM4k&6hrGR}bqDhM>4z@y`Nlka`Y?N{xD>h41_9?iblFmf71carM?B zrE-;8twFWkpjxX_uQ#G0@R8hj9r1@B&@=UK(5s@ek%F*U!d25yid-hi#WQnUIy29u zGjm+Nu)xCHBz8Q3VOn9}FrZo}bAN4%t9Kso?)61JxO$Ijt;zOYiLKot&32;;?TsS; zmoF~x{J9fMPmJK$_EA*dj}+4|7#q$pK9VDucoOofPe+am2e6N5^{WUxl8%q6x={1b zSqFh?90j2N6y>N1Ya6oC55*wc5(Uuk#FeSIq8!X4{O2RGga<=t-Pg%5?hytTqfvPP z{3X%>c^vqUk%;mXVhtRQjtIZ)`97s`jasSJS8i_*PsB;YVH9#wb9y{NU;KCaUZS=H(y{A37{@Q>j zC}{V6-tX?^9q<#-=)hJ}N`rSAf=!5+eY!gAh?733B%(}6^!ug~fYEwgh)5ga2SgX;8i@ndfU4H|8Zzl`BNokFWP#{#I1o93uZ9bO+VybX^6*SEX+(WH#N>Hm(TL%dpG&{uRh=xZ+=9zQSWJ5WnkDb zhKGkpBof%REu^YXjszSKA8~d4k6}T4QgWPq0jh`v@EhPO8pb;T=2invq|v4$DZk^E z$DKmLlIVZh5(}W|!G23j_L=nI1N^j?WgDVQ`Y;a)2^@_gIP=I~zJajKTtlkQ&Lfj$ zHrzjhxCtpjz4o{Q;DJURggAspq^rB&oZds;LKIX*Yh42h$f~}ol-fW{{9RfK{vInq5U^h;v$4HLt$wWN--ibDxTK8`B>n;+_lSD|!@Dhk0#6~XfUQ2o zeE(C5LV|%qj?^V=Hl)jMu?;H~aSzf~kDuV+cc0{%6f*H+rYM5GxAREFWQHEaROCbN zA~^HA2nIchG!G+I!5F=GK@!PH3)4oAhos+l>4OY(|95bUeE1e(;YC{+@#(k2zxzn+ zXG?2cKomkV{O;psS(Nt6(KQSerfD)Zl*JC~A)a)BRSIm|VsdPllk=0Dm>tLZz{c$a zUA_X(4|uS(N1F5wmXPfouD^y=8*!c2}B7*${-saGX~xIi^$bb2ToJd zgL6k)f4i&w1KTd50QST4ijbgBTNiI4N-IqSt59GXh8oJIpW3(9p+A?1I}Byhj0|Nl zO|#2D6KIX=dfZuBXJu`N;cS|*;hf+JG^it=$R66!^8@zFHSXMBjom`2Rj|1TVfc%V7I*$_+3*KjHWIlGHI9~h}e74>`^iG1#Z@Abb9UE2Y& z4+9ua!d6QxfVu~54{nttmSI~G$P2Xabq*Z+2wVV!pro)uHg2Juuuv{cQ74f}+usKF z;2{FUq;4RS??E`;5rWP^ltNUVod~!7*D-pqhG?blKW;RcBU8T`ZgqM^E=OJB$B~FQ zW*}y&eT@#$QvE=QN5+vA*rr7F+ijmxrHR{V_X>=HfJU>$Z{EMjnfWQwsU(SHl2j^z z?+4wgwrFqyG`>>dZz~MbU~9j?J6G=T?|<Ecmz;0Ep@L zVfZ=#{u|e4WV?yc_GLAFX+#iqW}hW+R3!a(A&;}g)PWbKf?R0smkekqnd}=4aRX8| zB%+Q#PwCG@fyfA<*mrI*#H7#DE8Kk&iROn#FBsP?v^?VP!QqZ3UvS{>!Pmd*l`XO~ z>IaV-nTcI~k|+4pD1)%QTV!o(pPl`Z#1V9W9|YZM!_|#l{?$)jr&MduXmxn;(rJeB z8EGST;CNogqtWirYInG|vcVhgT<6#C-Qa`k_b65B{hWbOQQY4kF`ND0;W$09JPQgy zBR6yraR(MO{GjcVFLiKyjY`{aA}wh*+vsHlo{6|XsEw>JP-mN9gf%PeK!Sm(kliQm zJe&$V`1)Wfki+-W$ikCBR`3{4ha>Nw$VVxTC-q4`&D-G7vofT%N()ayu~I`2gin4( zN+~?gXLWOz<+W{!l^WS}3P@6|H)yt`qq67wtZwY``a9RCH(D&NZt}u~lcW=I;|mq#CeLsvIN?Z6I-)zeR$g#}XNQ78C%8X5T|{zduIe z0T)_6wF+i?AJr z{5yCJ{pS)ff%oli&_e(|tpa$`;ds`>qZe2VwALX}r_0*bF6CN{F*l3jn0-Jp)%&9f z(bCZsDhj0vH}5^*?(zoZYJ;I%hNl)ChurTBFIGr8P?B8Y^qtEUs*j zO(jXiV9jYPA5 zf;`wQt$D=@nA&V$xjwqmM$M(A6gXi%+#siiN4 zL-reuhb_wbj%N%$pGPVoQ-Q6(R4BLimB<2!^Tm(=c}|KTkOSZMX*S#3US4BudzXo^ zEXicZD;TmNMVeCse$abpl!m31HQv8|ms?9~)EX`3r$+g`Z@$F+wQa84T;%%QWtw5T zuX?l1^7;-BHg|dd`aKdchj`2;?%4QRIyqPCO=^uMosMu81cB~1`T|6OEg~!bKSQ_x zj!oG!i9wb>AwEMJEp9Ydnz2Se5Bi0yYd(*4J>4G932 zJ3)UV79{Q!MY3VeFb<_=hpSa-LW76 z0tA8+0>uL{f(CaF1b6oY4Q>UBOL2-@@DLn=y9aj<5J)oXU;CUnVW70Pz2AMlUikj= zboGp7m^o|v-tRl}%C{=F?2NlAMW^msw~Ot$*7KL~$kM%gSI-YmFP|McrOav?g)5OEHodIJH1oYY z|4{pnK8t2&|8(+F*Qu?5dp%$%5~WL&Ef9kNcYyC>Hy+uaQ!CzRT8HnRHZ&ZcEv z?QGU>V)&ziIe#0SX=b^4CJ7DGhulo@c;)CC@Nsd}r1O)jI&Yn7=YM))=Q%m=6dAR# z{n?|@lXreHng8(a0F%-qefrM0w7o~qF-~0zcJJDIc&EJk7>Ex_nr=7f?!lh-e7svt z4Dz{Mcy1_XrOyRy?=|`OvLoSB`FPsCoibkw9y$Ktq>`6*`L|HqEu<&6<(1LDB8*HjB97SfiLz-4{m!J6Xg(xq0vDBrE%bJ5Q$#cPr<=Cvz|F z#cpRyEirl9X6dc2Hm0wtyElDe?H6A&#Mi;MY{}o+Sl_6By~F^E$9+x4cDBhgqqLdr zLQa}?&GF)F`k*pP`p4OrJ9h54ruXU_HJ?wca`=ePIo*y#DhRZf;49V@%7?3vgx{$bR(r#+vw>Ed~6qiIxNj(j(4*RN<OynFT85h*Pcx!U3|+w%_TbI5T{rv|Y(+GQ&H=FH}@QOTuCG&U;`K5&2fw>zKs?+>cqwBeN93y+`g zxqjGB<$s%5c0jwbrIY9Gu$oirMn!*{gZ+;^@m@8g?$qh=PI%Un6lsRauj-hWuVRmR|cn=*Gg{314L zQoBhRezM=+y;G*+8|!Xp63?xkdf(i;^~jJ7$t!wx@|=cZ_pS_naBtIM-NNM`Gg(i% znklO8E0bF9`ZU~MX^p#$uY1<1%_7dcKj^#7{>rmg>%v0vo+)nmvhUC<6_)6(maEew zd!cv3*Y;kt_~g&_u1V1$r`x9O=eFztyP*@b%u0W zx&HN$_+J9oUoKQRT~;DcCR6u8*kno64%Q8OtN3F*ASaoOKR2+vuX5c#e`9LmXsg9 zAkFF}d)Ji@9N%hnK>uGoEhgU^mzy1rUBf3Y9B;b6UFO&qwNA`9eyC5R|Au47t`0V~6>6>jsaj#ou^UX2Gn9F)42v*3qTpxEbjA+nj{Ow>O12B-K=zgf(pqqmZuPBkvXI$u+qAsVht&J_b z+ow_P>+^G(^>g-n>DlvQP?|FZ;x9$H?ih7`SH-dcn{Jm(o*6SV;)4$*#w3}n${1d_L&5P|M|SDt<6TBqGux)nPhmRC!{=MPad)oPbx@|LcfGUL-0U`@ zU^c6>Hyd>26ZnG5#sB!Y>H|0=&Es>qVkm(9X+IbtjF32+g_)8 zeoV+wtliMKllpBaja!J8|&PUrYsJg zSnt5q+Cf!66}Y#jK#pT=oHji0O#ixQ?&f(TLYq4-bN2K1INN4Pt7l0e4?G=z;nKV` z=3UnQdUM#|R;P=0Z0b>c`j9D0vo5%uV_M(zM`LnMepYpUh2VR`JLfSSQ1x)#d(DDO z;)k5ve`?8~mAZ0+ONM-MvvB(0GL(jEST^g!5p@=h*_Oh+;XFI6FHv^mqC)puj=A$w z?*S=kl4>7vs<|lZn;UsXozHqNCGPl4+xWy>$D9NAtq#gLJEd5B$pL9@TJ=xLQ6%=% zp1H}P1yiQ&=y^SN&$(|JuCPpA98%|Ljq;{dUuI7n`84Y2 z!&|U)P~`n8cJWt!j>@@Z{E}a8miN4JD!cQ;aR~$TX&)_3vo|UvrqKOulMAJd+2lQ? z*y&R@ngUY_N9XC%@P z@_J+amHQQmt;9z{uQRiP&e0P(&sxxBtqPsFoyPxcKL1bak^K9)iNVBS;sudNB$-fV zO-u+=Gr|*ugSIgUJs3PvDIaWPQ*+?_>=2#k~mLXBrXu56UT{N zgxK%4M0P^-z?ujkP7uiib=?xW^u?je(-OJ@ouJDS!pE$If02#xgemb)9rAC>=aujJ zo{-Na{OV0iO!am)4#=3<6M2f3Med?ykTp+XSlKyICMXN)YvoM8rkg=zCXDrfcmHb- z`JBR6TB0&BiV&{FRwwFAOp|q{7AXo(!j+k|=&+uTDc6Rt(asXXB@i)$yiXW$gAo6& z*IBtimnjgseC?pi-;w-xhAuPjn^GoJ9TT1Vrw*}GRfrp8Ero!onK>L?)1p9$3aHki zJBH5Qgh}g;Vg8|eSa|pWe%^crJ;yJBPq`47nOhjkfY^&Agvh{u`yn##M1&Fxh@0g5 zy<($n(&=?hIrX|Uh4i|NAmz?MCs-j>w=rWce|H`!!ed|K}Q-}DqUF4mzZ2}ud2lxh5LFbXPF=g{9@_irc z&c4Q`OCPZQ@+SqGE+^^NpMRe`c6n?{mYn(Zrt}MZ4-Kz*TWqJu)qm?@Llae*ery#v zf1@+CO3~S7((BTffG$Tp=)BudSKE`T4g_tr=&J(qR)CPl>KMiU=QOilEebLBzTar3cYUp$r` zeuVzhR-#tRZfM+T0A_7H1M-fIf{P!M=I*|hFl6?|Pl2^t>TT>CH14&t_`Q7p(0BT? ze^%^$I-(P~zo;{}{-m=b@9zG3omVTpE+2W#FM6thwp#3UD&%>A_L$+mcNlaTe$wkK z?8FAB*Fd;UM|sNAmaiA?1)~35RQ@xh@-OjIGvc2(BwmXn^sebMqHW|ftT^)u>n9-~LpJouIi(WlMm0ULV<#b59}H16g6h8RJ( z6Cz{(4To@_gP20@pRxHyeJ>62PEJHWdk`A$+F9fITW4cg;q$o~i9b-UfeE>H$xk~% zf1i(hE8NTb%WVVy=}T(*|Cpbt#y@a@oLmg~fGMnP?cm^&1`f`yu&}aH9_M?C9*A9AK@=zc z=|jG&)Zt$yCAH63oLgd^n7)FHkk3!k1b>NaV zy_Oq%;=gf(@FM=FpOz$&*L?i@ zjOEpQ&cXq@?6sjQEcRY_7iioI|GY2!r!At&ih6y-E`95e7}bi9&!3&hPy9rbBm9Um zL{Y+<$V#{n5_kQTYc20Ph+KYjbaK&;TCfA_FTN-5#{8>tu;JVr@JH%lVTkputZh-Z zZ67Q>atB+leHeP;*m2nTyZPYKwlr5LYiO6>{5lKib@t-(E&Ij_6Jk!}{*6VUM z)T?$_xDdVni`=Kaj(?-^FScCldv3;yb{^#4o;g0_`n>YKM%<^qUgSXJg|VZvms}gc zIznO+Ipnjt5xztnVk9B8g+qkciF<_ji>Jgh!}%f4cZtiyUgB3mu6I2`-bd=U!u8iO zA&>PVN1q&=oH1J{ye#JIxr$9!Kfgw^8S>b= z^Y1Zm##*?h%c%N8oo*TbEmz{7I3&Nh&Ri>5=Mh5gC8ihd1%H=&dA;yY4wXEB?;$ax zN2r?1+k4XPx8`d!?&VDR_?mxjzQ!)2a4-471j3fcNC@YP2&pXz$7oZd6)QZ9l7IQ0a$ScK zdL=f?)!dMW|2w?%Z#3U;Y&UZAJ)Kw!FtcHvUx|E6oJZSVK7zn)Tsb&!}$7y$!I*i!oe?tbo=Dz^nL+beA^X(W@dNB`7&Goc+U-+jD z5LpoZ3-avFnv#i`=)S~9uL#MPgd?TCZe?wQwC!&wjeqUa+EmnWX2;KUQ)_#=>OJx=g^{ILO#^n6^8 zI0NK0f@6kroV@N~GGZ=2$IMkbQMpbdWFnuY7G`2wq}Ka_|MqP{>esNau!MuXBb*(a z;cV{&Cr3wQ$e0Or!+T=Z&WotgvO6p+t>m*KedcWF5HSVo&%b2sm!jBw(fcj*3G??{ zLzPDD_*zTN$Mxm=TND4=M=kz?Pjpt!dR?|qHAi=6d?r0OyUdKi^0MAnpL~g2hUp$pE4k~y_!L1okr0M z>7R)Ic5-n=P-tC@oV^U&&fcRQf5b`QT=*lGaSv6Tq798DN7&n-*zoX0`C^E=v1#{YXCwpgjQFp60 zYKxv@7hvJR+pGzE#E3<^Xd?2MTeP@?H~-g;2K<+v=`3A7lIvuhwHxcSoV!qME$uI3Ci|>lPVkvyjcegOm2YET z(V06A3`2p)y(lzb8BDG0R6ZpK5FJ2-*W9O~L z{#&mQ_gK|u;aNC4LcKn4_Y+QE_=vL?lW^e(aCs+iWi8KZh*iKP;v#Qf;A7`D^Y8Wo zhmNKo=8_)KtmEyzodT9}=^qTT#D62^$}{vII|I236o#dxCF43%$<;Jlt@M7nlK0;j z<88S532QFB$IKlUF=lxz`f%5}T={)q#afSh2?y~LhK?0TJO zN0<@+#38+%hm4hJ>y%oHd?tyr#uDQ9QWSX*dn_?|D)(ROyT*Sf*!T@W(FsRUW&2Bb zHKYEsSEkf^@)_y5vA!GgFS%kGU-}DE*f}_%L3lSrpL;+ZPgZQaVy_kb)??R|SD5hY zHnie_Oh-r$D3CFKaDz6? z`5$4*`V**7y%92I%?XKBw7)evOHMkO@FV`Iyr2KHQhU6_v&32-2#G^V6ZS+aLO!ST zc=fu>LFAvYf%xoHIneCCJk|xKMSCMxuT2Qu^A;6XUW8jg*6^jT?_Pn=OT4e?J?qJu z?#o$=2P8L?9FhLBK+&J@+kx}so&KA4UIA@BFE4iAe2Wf)$0A#n+$fVP5OKZZaXadx z!nN@IXSmPjA4KVKuJ21UNjC$*R-@6p@N7&RdmOvzr}y3l4$yDJiJVdH=5CDD=g5;k z)&5)C+M@$&{-XCaB#tydsZMThaWG+#W|r&cz|XhhBW-TX zZnPQ?4f_3Oyc<9Fa16Qa{sawNr@>!00wIQA3Vm+l55zxl0*fjBuRtwsQY@ zco#0F>OS+pqJAM*aNriU+|;M$cFRw`Ky~&3ozrDT#KLIG1@k`2$Chh)%-MAnWvbMp zFAy7&>O;i-?}?Z8Pz2TB?8ZJG(FOfIw^jZAdyFC0DRy3_WQ444inJti*R( z+%A4P=58`N44nWc*4YEStK&r9_&?QmW8Ssf4@T*+smWzju^y-JA7nBDL8c>6C(9H} zn{gI9ZzW?lZ9okFy&fGXZ@2lKtG zm;T>p{udv5gz}+HBxgkVnvEG3CZx*7rmHFJ6~9A;8cq2=^o52v!f;;9^FNXI^Pf#U zEO|?6U7FobeZp|Vo##1()M8W{X64HM%umc0sn_|s!YyzJDlE97a9?xJd-%=U4|~?* z6q_MEWTlU2;QtHzFL3}-h;opl4(C0slzcC^Mgzp&c%|g>lFzHYSd9Vp-gpiFYV{QU z+xYdu%@GNIng0j0`wz!52XAo|4c(?IoNL_6S?oZF%@~aAcL+OgCL{VrB7X9(tb7m2 zLn|`ATgm>v=(Onm=4+o=%lU-4yKkUKi9m&0&wNENWBUd6i4z$I$m>||p&uAFZyVeh z|Et$fjU{TgCgdEI>Z z4RRM!eZMX1KAlI;!MZDq`wVtpxZgq<7_o2{+%tJfZ-IHe1KLGQ!>Y4yut8>l6gkmj z)rsc_sn^Qb2gr4l+GY#lzx|M2h~$cg3F)1P|02jQ=Xgx59AT5*6HeT{#L6Wd>u16} z<8 z6uQ;jh;8iktvme~UIlywg&f?=EIRELEp7m|Yuh@y=LDey5_f_@_ zKahXQi}(uqj6pNj!^JtBA}73l^1gB{hZEn!A$~&oA>y-c6H+TM*aGz|bFns#GUvis zV0ljX&syX6{1+nUg@u2y{e(MxgoO>vtjtlnS!<=Pd-AD4=Z&~mYJIHFtd2Q}!o^C! z(Z&f4eZz2TEC-zSi%^QwvhZwugKXw z6Z&yBa4qZq8x;PNl$c>*>|N%yWsPe|($iQ;{F5;t|M|ash%b+dP5EcVvxlTBC)G176_5pvTzM{t(91nTvr{$D&PeFH|U84W;t= zqgd{;$eJ!6(mHvc~Z{)Xh!c2_i1=RgUwTyCiPt9d@6PGcq%`QzOTTd6 zEeu(D6yZ~Mp!MWvRP8nnRt^qIY@p0~DfI#a|6=nsJCL8fKIuF2ddZyRpow!C_p!G_ zex<)7@!u%}q~>od2PYT{E{;BcG8L=9#=!>mE{@1ov?OXYX@khAi?M9yX+)oSi2XO- z;_%&%N)J%-M+JO;IiKQt2u>Oh$H$H`=RCr^?!|XGKHf*Kz{iKfDs`ps>do{P0 z*nSP?!6FxKgL7J$?@m*N9YX7>*(HqcM5&IdmF63u!X2uckh))d0Hj{`YcZ z;|G-(Q2Ju<3$2$jGy0^y$DA0np0pKVlea6V*8R`;*X#gwKPP8QZD=QWJv_bgV&m}} zSFQ~!qVtNC=sytX)TO*ryeiSTK0q!0-jD5en?j+$ud=H1%sE&lhtVo+F ztG@4?X!KjS4`GwG8MvqZPuYRGy(Yt2);uWjUp4Bx*5@VnQtJ`FFMc4Mh+$=KBptUH){l0f&@SlfuWa%mJ{XBE$#q`zNlxy9%T~`z?kOl1; z7r~Bk>2Y$p9WKsxz@<43xI8}{u5YS}2WMvE<*TQ7{SkQjo;}_d$+QbeU-*arDfjaC zVmpq~AB~>9f;0SSl^8|V^VzenAw6BKE+{(gQ_7FFJ>zE?u(D_D%UKHhw5lKc(mzPt zcZ|qEd@qO8sbnq+u4ywU>l-`F*h%ii?(axN8^!=l2G4;b*E}d|p)&=1)_?K)qW6V( z&Rjv!ebJd(&DvqZv1_Q)A`I!$+M{zVI~I-Jiv?#8cnHA$ebI;t`?u z;Vk6X<@?Zf+IF;>EVlnM|KU@2qItwTIJ>dVA?qG9l4ou1ON;xwTSDiMRk@aKob8#o zcmw*5nGRPsM-=k1!{$-seZDif*Wk$84hB#TX4~M>JZC&P5rvdbZzuu4hh*A;=c+&d zPw2k(9>Txaf!G@_F@bX^{*`MY9rJq!M`z9;W<=SFRk38(VH~>i61AFy!729vn|7?|EVs1@cG0~SP=dT~mdR}&}pK`I* zA+oLw&dnz8f5^Ycf!G3jTwUsoH@7&{!$VI)aObs}FKFETXU{T6n8f(<5q@5|4I^eO z!Sq$p*m>arrb-!-n;1UME%0vvcOLHo31eo-#0kwJ^1o$mga1-;j|0+V^mX zoDLxpB+hBvz8B~4?jvf=Nl5J9Y35#Zo)v?Bi;v=$JvTW2vQOee)&8d`O8X~s0tVkN zvm;It2kZ1s4)zHHMooTDt63Yp#1y$)O)$TyB~C}#UkY=H6eUNIJVkJVF{1o?agUSGJ#sv3)p>IAPalw#wFBY5 zF8Qz24>~swkqwFcZW84Pv8&(1A^Z;_$iFFCbm)!~58h+{%@5de?j_bUQQCCo1!uAn zaQs0MR_r~b&e8I<$}DeQuJaWAmzuvoW<9hGDa?)PKRXy zCfHrq42L^f;?zhxT$oKcNVWY&-1D(>j0-O=$b`g~mlXcrG9Hw@2g59IB7#0)s@MdH16^==kvlFi20Tyx&(3nh+q=6I z2aw1Z;5`3*WxkvI{O5T8*Y73#NBq1%sYxp_qUe7njeqKY!K!LZsOtZGUQ37Xg?xTa zu~YR3srl=RmMEj&5_e6hscF|(dh}AWJ9H-jLnh5+d_-B7{hTEK%SZi}wFENX=aQS( z!4$3;(qq>8y;!>EB;!8z49R~9R~>rh(qULW9i|u6VP#nzb_DCNw>ssZxh<~FE{P|H zN8$C&wRnEv7u;G`fpLHvUSC}bBqt&nlnIRK(7UkFk938MvlV`S*3! zp>1{@dgRuje_kD;3hFSoxDMMYTjTQ7ws`;GJoG6{_T}AN7D>rju>V5jMYTTEz zDQh5%dla1zy)b33%n{AI_EYle^@lHWE{3t9)Pd}DsGmuPu&g?C$)Q7U+JeY@rdZ!G z1Rq~;@Kc|naer+}_*)Z}03??Ek&{4=`rhT+WO?R^xzw5erJKK(Oa}BI=LY zf-bZ6vgZHk-|`!AoE7fRTCuI8o2vVw{~iSx`&0KzH09nwTwkmD{yjX)-0k;vNS$&t zA>TvK+E+@&(0U(d{kC0y*t&32UQS za^yk(;gd0cLoB@V6;Sw3Yo$Zg3_3K*tV1gg9Xin#4DofvxeW_x-x=H~)gDa*^hkVu z6IT}1;`uUCQuNrdCx*48aI89f^RM&I_+`uaH~6W;0_15r1*^E2$k?X+EBzNcBl3TO z@qeEv);%4hrb+JQ`+5|DuI5NyiwYtWqWe-iA4Pm?){+nZS0Cc9IuNo>K*@2<%`Nn@ z4zotXmZ(rERIP{Fa)wjcGo~wRe5`w!vJPg!SpZpAmc1I!+}DNqpQ)uCd-2Z5nIkXK zxVh10n!v(T2Os)?n(nF{XzF2t0cEq}M%;dl^we|m$M?9tYz$71ZGn`eL?k36piR3j zT(9Ae-ws_@eEGlPPI;dFq%|j=!KdX+Sd<%tDg%FG9`p834A5Fg}*GFVMmmhQi%zK=9O;o*!=h>h5-h4klvT_9S|Ifu%D-avU+mr(Z zO3agnH4e%3WnU-9oT?s>Z(TvwL8b05y`cPIF!c;U8usa(nJ-&gS}5-)@0EcyprEv> z3^bq}XkWk$$2KlA=)P2Y!J6Mk+>cp@gWW3P$+2z9bEl3SL3U3s6!a~F4aejEGXKX? z`JVw@>He?}9){iv4&lgM#*pOrTQa2OI(siZL!&l5k(o1V9R>_T!`7XYUa@=+m4E7e zp&FEbYq?Gm`$ZG!iSO}{8bBBF{=wSZ1U~89(aN_33bGbznL5uxd5}GR+*s>Q%if;3 z4RZkMzwG6w)cw0t23P~|=>;9vhng1c0?WJ&Vab^rOKWGv2ADAq$mO6z#q>HKua7PnKL)fgXD5U*`QKhyGq_eqYO|?6I?o{Odiqr`gC7m2r-<@q5BMqhmQg;1btL7)~bK%2$N!MVd>KpZe_wyq;^-7Z`Kz9jk+OEsj6_w z;Hktf%6y~LxXg82@9)9d4|~1Z_hx*fb5ZghE$5%W=VV>w_j<^Bz?Gu^?$&mg=o^aY zzz*16u_I1}_r=RmWASwC7{vDIjz+%4;A&?FGui^aUiF2p`4|s1HuM4wen4cPh}Z$5 zn86D2UH34L;5r>u(4FvW?S3Z zau2S-)YYfT4A&oV{jJBvrmtflpSkK_nE0|@;oF)vsToZ3)nzQpm^FJDn0OTDUKU=A zX*lOC@vPW)v2*;64ld3p8(fWb4B7*Ye+wI~nM_^dFZ~{goxWGw&yTLmD9XIbCoA@V z+TZwLV1#b#o7HOk(^ zlyCAMQmY|44H^X-&M@-&2)I_x^aCMlrewxb;yzhp^}QeE`30T`pT4e{v9Ww>m4A_e z!1g#8+70ntBatv+3KAww!P!CL6M~Q{Z5o(~om6ZA>jMtdp-km?jl;+@eL)d&??XsD zATm&#zMwc|peUcqSzTQl!hBlzmiSPyD`Hy+=`px)4!mBA4w$oXpR%{lan1rA;~b~> zfxqJm)}OqOa;-+fEKfC<_kz8MDB{DP7g7}_wpC4a2WWDlULho#8gDJ&oQTIC<&;dIFDF-1Pa4wAgU|1BA zCr`zjaTBn$T?bUlpC68#lTzbDUMIPRoW<8=t*GXNk{5^#Q0xG?*X)4EgLh+?IA>S( z*R$ljjVW!f8|P@f@)d$T=Zvi^t$B9l45>FN*KUk)^HyTr;fpwU|2_8$`1F@$K=uz> z8gm7{q3vOnwInRu3#&HYiECc%(sEvnxs7~ou|4cxm1VB6Y)2gT3weZ~8g(??m$TG* zMDOLijqoIXghOh8(z|#gd)>9lSpv}kZLy1Sz^)*b{~dvCvCY2?Vybn-wf2LNFlqvl zr%c7mF=Mehtd$}MF7(w(t|0k=)JYt(GwusyY!FU6K%JL(fcasn4Dh~5p>SAcDZ%|% ztYBs9fV!%Oy<4f< zd?T+{`d5qtL%7MtsCDjIB`%B|RA%a9y?1D^=#@=u@Rml#M>OP)F4tNKT+YWPvi^ z!?-+SarTz1xR*{YH0C~RnXvQkkl&{`D%EbtedAU#|9`^T zfXsjZeMU{@jJU&>{HuHX9O1Q+zR8btNbPze@yU_(mfm^GQ~%p5{O^`HfDk{Rz&{+@ zE40D(z&1G6usa_1AAwI3ry%*~X^0;_3^VH0MU}jHVMm=dXYD}QXG!YC_O!QISv!_| zP>Bgt{&mGE1FRXDr!S<$UrufrxbBWIH~H4(=IaB8L05);q(CV9PhLvyZ9=`ZVU4DI zl{%QZEE0tP$^CPZ{?&fE0dsVp>iv8amsKx?2858W# z`~mHN*n%BIOyy2E)2t7kN}e!bGTu*^g!rM67#&&z<>|W}smGS|uV##`%ot-^IC3vO z55{5n7?U$EH1+MS!kh8ALuRg{V&2c!>Lj+Luh#hwpbQKphH&17J|u5V?kD9%`@r=Z zoX7Ua{f?DV0Y#;Xs6rA950`V1s6AwOM#>&mytBN%q zW81I%E{^$j@}KiZBHrk`$n44#BFUUFZx8SC-=3*;$E`64#trEvYs$M5WoMU={~=b za)=EmN!%s$`M9QicFFqKNk5>}1SkX1Vgn5PM~f{WcJjVJ9+&f>n%!`@@Y;MbS1sRIa-&ck%J8iyU{TP3%gm%9k zbQL&j5g;}o61rf@Kt&>$_luCHw2U*kH@gjc^yRsS$ZxS{7!!QpeqRZgv|SpR`VRbi1rsdPZ=Qh+l}~FZu9=v{O_hr>tskl~aAp35+<%$T3?0&A1(75G)HiPf>Exz|?$`cIsN;^l%dZRs}l{yrFN z0b>I4fBZh>;eImpJ_(2KBy#_Wr6}WHiR;F_Va=TO3;xxde>XpG{=aw~zvq_~xq+a zq)#6VtW*(sGiKo03g(KMtrgDc7j&-l1u|T=7iZQ6r6#O?KFR&p5jlt-^$;H*>z5i6@1-74hW2%F z=_aZEU$~dLfHC(f|80NAzfwnL?I@;7C!B8FgSo;mykoBLjy0tFBS&Co=Z@%5t}HS+ zIWbS5uJbyJ+^Msclm)r&Qj^We{LxS1KgxjE0g(ZJ%7Vy1u*d*WhJ7CQf=W(rVeNn> z?R#U#dHya(#8~p0j%)keP`HF2Ia*%2ZC<_u7W~2;|F9+6(8nx}rHRq2IF=Y-saP1&ni_Hm!FYBSfgJjJNFtGL!bfWPJBqO47qecxT&(xMpW8}k(@ zrmR(x@2c4QY$c$pKALlW<5m1bS+Hb1N|i_H0Z0u%Vx=GXkQ&zp0=CT8>Xr;e-_|3s zp-MN}d5H%Y^BehmEjG}&7oCp@Zb!~rllw3ns?)U=_z8$?rOhYfusg9lh8|t<03~PH^vF&QzI!kf- zmHyC$GS3X9{tH4VU+KNocvP(e%gl6U;zvEi{i2h2CZtXvvLW~*{*P2{gQHd2Ag*d_9IMhAM=H0*@zA!o+`Jba_YuD^ z5qjp0iL5I>9vy{MVXe_SFc1YZW@JrK_H8#&^-$!%jyXey0&0#}iZ#OWvg2x~Z|7l8OZplr|L1u2C4R)ij$f)Fq0|A)7)yCI8;BawFHnE~dxRfPK*ZW}SUhkp zwpSwG6~c(tN{v|R#Hu`mE4u$#|65b{!xjBMQaKFAtAvsNFr2Iwh7*LGMIPd+hvOQ5 zd-+MPq4+S4axi@wFk?DCOrC^u{rh5UXeffc^T2`rTl&TPj0DdXHq05wT`txSN_0?t zgXD}EeUw?>O=v^?Z>RWv)&6t;zmRY9f2w*IP8&h)pQ*t;K$~>K{T_qy zj&mo;Q>U=cG*O8WZbS^lug#jFP3cm|N!=Bjpv)nNJV+ncfpL4bvdkG-PcGaTrnasy zqpuIH-3+_0Jk`fsd4cd=k(}$I?GX8qoKt4Hg!_uL^OZ-c+-ql%feMr**X*jSDKe-0 zeyrrlKhhz!Em;fln$TOirANNDBT#MoEAIXOAr&=aK0wg$zz1|bmxLkvA7ke9Xl!mU z0K3^E-p$^Sa;9I{6BLfv;5PLCZE%1%R4I)1{!9HA-537PhKAuB@%h|Zk%Nm(I^sTi zzKK!eSx=m*^qrG{{03SIB3NG34{Gt^qu?*_kxQxKjVB2$_DSBuGJb>8+O2hPW|vUVl+}% zLjYq%$sgZO7>_H12VqX*MyOu6AhNi)D7^-mArq*18-I7o%reQ$%JwbyHk2NIiW28b ze@XmBP^4N@)ar^N13|1Ylx)kIBwu4`tLeVje({SHh##Q_AlyqoYZdWMVygmav!b7W zQ`Fz^NZI?_m~-RXHOPOBy&q6#Un1&J4%(jjh<@BVZ|s_rSk!MWHZxDyCUIaO^FH>9 z6&n!J8ix(~FM5CKE8V|9zK!{p+ZPNFSvX&_HLf-4fXDp3zqdn1;uB-V6waYYeK~RB zWZWG-0_)m@BfNBJ&ZoJ;-cp?*=66&16Wbs;y!7&1y;5{V$*S>0a{hBMDpLdq2$i1*d!` z-N^PE^Z)g2`Hg@E2NKbldnxwhZ>3D+KB`QI{?+_pak@$t|VUAaSK?@8v8u|2iRSJsb(qoA3FM z8gBq0vn+`YtZ_HVRuoIhG-Z#k1JNGQmD*!&yNT#L?-c4X2B^O09jXyu^ZxbyIwjE6F$%;+=__Aws!3?FengZJ+s4|`@Gp%+Ccmqzq_j2qTj|K z_lm5j{EN*Oy_bCwrAPj~b1$<#tr`DZAoLk+ozOW?S!|ZLjrqIO?3KEGP;2a@-(Fdt zwb|k8(Qy5JRNtNOFZmDM%~+AB%Uq$w@sH?zO^?C+t&B;V&SKTT*@$i08z*Xny!N0pzyEX1KZI4H7yW>s2!T2zIG?E!Bh%CHg zym)tL1h%wkhrz*>P>C~2xw+qbI?kHP%$oQ616eGUp zd0&xR4`LX<=L7C(my*xL9YYERW2@5RRe9I?`!esRq8+w|bi&*=6VZM8VbqI$g_?iS zCy4K;y+0AnnQbnW4-^(zdF;Q+5_hHtn;j?-5(=|Z$Q)a59klx(-sIm=Z0V37XGUc zRf#$=l!N1+(3yMd_mwy?_7+x6U5z805x7<-oSZjTQHy5Uj;^lqnh9SOwYdqxddOi*BiWfcm;tl81-g8~k2hOi1jvJ2z&M`k8 z6@^E{laZtGGHM*_kmE7CZe6ZfQ~bm$UbiO1_Zahwy$>WfyT-W=#(5316~@vsO_VzA z@8-JL-}yoY7!OFlSE=nu|0kp!7PKCNzLWN#;l>B3wfo&S?SXJFyjM5)gKE2YpQy&y zH#o?=@pKaU@wZeWk3PY?#k;U;z<6BX@8{m&Z!O&TF2@Tg}$-0s>73u@ItY3_GL#S!~2HIdeY z#LVB*A$5F_TUn=?>}qAp`F>w)s=#%g!YTdzzvf?kjmH0O#s#~GEtNZCO0W57yY#9e z1Jx9+KidP1e=Q!Ux|949m3Hv=5_T|_+`->iIQR)&F6q(p@+S;taen5S1K8asiu0r$ z$$fL&7XAIZylbyF@GsndMl0Nr_hl{cR$ab#9iHnn#VxKgyGmRnR)p}ksQJ4Pxi|}8 zEir)_`^WM6$zDm{lmAc0&nxRXo=IGj(;*F_e5x|G3+K8nsnh>;{+0Pp{*CnbmA~UW z&`#EN1nX-L#F#;=(Q4^6)Y|q+sToKvDEx~|e9eDl%77qb`#V&OP9WaUCcH(%Bk$4v z+$Z{mx9A=B6u-_`huF@;*&pnz*zlVY-_hrbO^7%6c*W0CpKpHyV)t(e_lD=>wOU*t z?{SO2`7Y02<$J~PcU_mTHyp~^zmO+P5h z+$F_B^J%rKSb@F2`c~m-AIl9 zY6ksR_^0k`{0DA*1OF|rQFikyl;83SwRb#6i^H$c?d%&2I&vS=w;acYY0Gf1$7q~u z$=`>#wWHs|w4=RK_RZLib`^V?0qzzKWMVErT1q&=v{-;b=uyiw!73c0UrKuGjE z1a5f?zfEsYX8miFTK5XRt6re+iWfZdcU3mOK~2__I-E?Jxh{c-B+x|Ete)eHdk6YmJ_m)#VrTnYtg1SU0G-<26EezE70_jsJ?< z5)imq_^!r0BuFn!*9{<@C)>ddyes2 z&ST1~XsqZn5lbsI#JqBqF|Skr^BR8)DpVX@_*+obJaVE8f5$D4?Cou71sh%$@rS%d z64yyx?;UZIkp4$2B0C}VvG3)eo6@-wgNb_t_1+A1vKGQ3S;t467eD=(Kh^JlZU5zY z(f{qV=gK-Co|U+tXDwFH{zlGw1f1nk`@GWY{Ss2|-&MT}mUNhakx`q_X88@&qR+3) zeo|%nh05C*2X1?VAliNZjW1Dl{Y#WwL;hEh{}sadOXMd1o(rDAWA0OAocR>#rawW( zS=`fM&SQ8kda;y)cr8s$#r}y8*SMD3GQ%tLTMvno z#6m)5xe5_Z#P_7{{J2U!Dr+|v^Siwv^o|x*sO?b@o4HO;c+%oDmH*FruW_l(b!+`P z?Y^AVd7dxlIW_KMl-WL+?^Anx`+{Y@Pp$JSwSMw1J>P?z?}@F}4x1aZo)Wnl9TuNQ zt*y^db=zxH+4>3=7TkUtrYH6jy2sk)_dMoVnL&I9n^hwrtkBdza}GJ zpGEig5;3$5`zo@(OP=>vROkHmv&SRQ=KZASFFn6wl!K#H=^q;R#k{_=(0|4O)MJcL zg}jH*?gy-Vj4~^q!FSnn6sBH#FMa{9h0l>~-g9K0MZGuh?>g-v943?di4S2j@eyoC z-Gq7ZCdU2w1YVOxL=~bG;YDO3TnHu(V${6czn&ggdZD#co^s`n~q z!nwx3u^tQme4NnqUHpXbpSrGx{C>@Q>V4||UUDz|tFyhrf0zn#f3T9wduj8Y>deP6 z&i%zv2DIMKshaJvt<7*u8@v?VryWAwg;x-~>>f(}`T)L5o*>VnXUH}G39`<8rtqJE zwqNxBYyK^}?c&dzPRnh4)N0?+&!-#oFK_QrxR?1}&UUk>zn?lT{2w3=8s_{C%dCfC-a~r6 z$2jvRbAK}TA$|Wd?ERe;)TEp=>W*byCZgAv?Wi{65=zZ~0Pp#akekR#U!QsQQ)v8) z{TKbW75$&^5SH!MsOv;^N}uL2uSp2;z4>u|w~=)=`KSHQFhkS-M{t_@2o95I`zZsK4JLCwhqGQf zuF+KYvKBl4qaV_T9?AKp6ld_t&?-&A%2Gsp2f%i=!f_=fAas_mirdf z$(kQq{5gkh(BW^NB^EH^RI3H1{*BRd#;+57CzO0o?7ku1j}4;Fm$^QTe>v0sf8qB< z2FSfO^C|XU;a|>1^Izf%lvxm-FSD0_m3>{Q_r^<)mvx-|O?y!mrlIYijqn?D64|HR zMcNrp;rjCfI92bj?oZBrd3aH}65ne)|953p_8dM)8PMl)Ov7Bik%8Z@_1^gY*ZH5w zg!a6~zr02sQ}3&BzQp?U_k28-wIJbsUvQWLvHb@KIm>*frvDQ6EAzfQA0>qQZ}Km( zfY<>8E>Z@>7hEI%A_I3=uNB;49rtQI#+FTc;@9@$(ZBBsG##)GC4Z`_tn=Zuct$iL zestf@uc#c-+lwUL+i*s)C->};b=sf#)Oh{+tnsX!wdb|_U-BPAIZ&Z;zhCrT<6mNa za;rh*T;hI-13vRFzCerrPSPGIHh_Gm+5p}@YsA0I1xu5$st#F;P zaHMWHDtY^A^}Kjqi(o=*7M=F5JF!=2w^i}3GZajrl*J@o$I& z{u2M}?Z_;L@PDhe^nqJ4k8FSn8MsG-%D>e4Brp2?v7!sB`(4a*Yr0tIR#i9EP5oh( z?uf4or@+6n<2-APOy1bSIW4un|240F_rA)%ah+H4J@NZr>%I8@LzD#-qW>!Q+F9(s zG5?AWFs%Pja=~xtzh?h$8uVYA{?bxd^YTbSua z1e@r5r9J+`9EHgLc_uyjtlax#PKmnzQU3QD?EgN-{js#|sk%>2g?A<9le|y(SI*@B zU+KTZ0AKN+>i_Bc#s13-u*`q{8UMROY9b$J`^ltWHSy=U|C**Ix?w>-4Dp69B0Y?8 zL?ZX<(6`7{92)~#Dlx#9eZPNC|5N!-t@EpOzEph|&NV1{ulj$@?rU{`FZ7?U)BL}t z|C0a7Ot{STi|$M8FWlcEZWG4(e}^+459kZF22?{12kzgi)_te&{3EQ(;n(hs^j&?JL=m zJ;3^$@7<})c>R@pP?HOp<1n7(FpB>e11jq{Y5&z4-#54yo3F(F|F6C4fUD|Q`iI^@ zKoAuL5$q`TE-Ln}*kTtuDt2QRON<&5jj`*q#2QOtG_e<~QDcmnsIg;DwZ$k=x&QA! zd+xbhkVIp9?|skx{pOy#x16)HGdnvwJ2NZv=h}=sVt~-xSpR<`I-dbncEImt-ESrL zFOm-ujd3ruH~VoJ^S}7+Esd;>02h^)MfQ8CismfkYn4N&8rbHQQKU&g*={F`HO69o;Ln3)Be*rtN{J|?8J}rEaCr+QIEL1ND)8A@q|a> z`yUyQ3#%gAa~?40`K&U)_`QBL_J0zr?7zkwV9;O5duur_^E>ND&GWp_+gRs|%>SNn zf;yk<`&#Thq`lbqo<@JM|F!2iv*P1PT)!oVpVynB+rOnRq)(DP=nvW0RqoL zf6)c4=`Z{LJO{|I0n_JG(Vx1%p$8nIZO>ZIQ#oGJjQbO;i;Yjfu=mJbpOXEu&qE$i zc7C<)`)m3)&Cc@#_1xUz-U_{tW*Q{S7;x()CE^RC7J+O4qldzl?pE_Z9ukJn(n&|02h|#Q!C7U${|V z4uk$1h^NFmz9}g^ATl5`;S0WbqYlJf#O@E~r3|^|FkcWLNsmH_BF!XS}o|t*Prt9Epl0 zF~+|k_jNxo8UHHgi(@^--#5huD0TpY{?_te=x@vch8@6q9cVoV7;8aU2Uv2qnC-w< zsrw0Rt_7hJmZ zSF&H1{|5cV@7M76HrD?_e?$JK8vo|~p1)cLh#kO?0oMEfY4w0Br2RG8{x?O{hrgq% z&|l*BW$*V5{n-i6SY%z0gD{nFoscZ&`~)%=WaqGwDmR!q|5fGt5jh~|_sAZQnf_+^ zFLQuy|I_z;t@r(v3^3b*EONkX2R3ZL*6YA$rN7wyE)zbBtc48r4x-E4Vf$szwAp{f~M5W#3;QZQQ5TJfLg= zR_j26{>B>6*!xXy{HL`82<-(HFvo|7ot@DCCfgoOctd}7!gCawhEyKt)x6#l9uNqi z&A1l*iDjKAxnU(9lrq4?b)jiT)h;Go2NZvRjw}`bpviDz$AbX#0%JcOAyHVLsTJpM=ApSHeCvFDn zhoyKXe9t`PFu%-Ok2h5A^q6PBn=EmF9Q(op`)WwsKhJhjkg^|Otq&M$0A&Lf`@a?K zPgCzdOE@R`KlwmtpF)3G6SSrM=q=^*Ijv*`$|!&4X60JeM&)v;#P+_SKRe+Ci|B?D z95IG)i6C==i5+W_yrFr${P9+fBBs%t8&;KXjRg9a$+}Q-L5P3&uIgRM3&IcP+%IN7 zaM2AV{!8`)m9Ll3-mLE#V_tIkUT7%!*|@$F8<6Y)5q@BOcg?1lT)Z3_@*E$@+a*3< zlDCVrm-XL8g6Olt!*9yV=>g#b$#*@G@B`sKLHe5VmgfS|$28R47YzdosN4e|m5Anf z@vWJAgmK%n6(uk}a5s5De1?_J82tq$23+W`e7q^!&k)WM&WoIvD`_u#I;WYh?VH-o zv68;M)@(O}CYqRKY%ybT0oyDwb=^a)^A0>k5=3RYDItI#`IIHjRj?^;+-O8&>}}Jl=shqtgi0Lw@=17 z*LViq#nyY5Aa+dI^XW~Hu`fEze_v_Iu3gg6R@>a$R-4oAKe_!P53&;K5yZDh#+=L# zGXBK>M(CqJ_(Jl}c{#el&)JiC_xx$64M6FPfhfy!1S|LlA~I7TDw-i*h78E)>;z8- zNAcyd+{d^duVSYX9q%AvKA{sKoFHqL|9+YO{Rn1f9!6CDD~jk?Vn@zKka5?UAaOTe z5@eqEi6HivB+e6$3CVmfI=V^tA`QrAqQCKXDlebVS#o#DJrW5!2>&AVB}h)6oCHsT z@Zy{DpV0%t4`O%ENXSR1PUt}xPLO!KJp}PXJ4X<@%KY&Q;Tqv0;RHc`vxFdWr6)mX zU5F4sa3jbw-jx529uRpf^pY{^N)Q<>bQJxJlbu$MP?k`bke3ih@FBPm#C|LP{qL9Q zMB+_((*rNk15hO?NWU7d32Ex_sq2q5so&N{@>R-XEt0PZ>J^A*d!)`$$ye*TlT?$W zxsd?XIaR+c^^vM>C*LQU>yP!1d0YLB{Et6Vx0g3YUB1>vCXAGNB)?bnVVa$}&d2Db z6fgWWb3&v&-mjmr)GUfN}pr`35s={HM}x{k2^QM&bb z%g=R!y+Bk=%n0UJ>IIdikqzJ>gOr-AV^86t7=N!Ogzi?FI>N^KfplWCQ+{& z?YA?3pJ4uh-JkmRmbzZGaIcx!EYy8kT_1~P@y=%$b%R@sy3zlCRW~}s_}<{wXRgQV z2wHxh_>{Vt0W7~yTQ@hz^1Zo1DfI;PfWNNmH%$3n?*YC3uJpRq|5ZIvK8RHG_*Pz3 z7mieQ{e@raqRjl+6dFzbqU7OkD-z-fHd-)WSt_d6f5(gHl=e+*fP`lL+6^M)V6~Rd2C>ud}|H(3D`bfp1@Gk!K685*zg~&w)P1 zv*E82e&&0zPh27-5NZ=*2zN9Wo+}X60$S)>%rR~+^p7GuZ;|J>B>ZeoduLFtaMb1b z_8o?Qh}x}t!oyqTvUo^d;pw7j5s<$A1Ttqd(kLmjs;=FbnV4=rP1 zljh(qf>T7-ea*!mT5g_^U7$DdZz?&~rERh=^t`12h1suR5ydfh+G>oPw;t2ie}m6? z{@d!KcOJHgi%+)W`7FG-^s~_DWtF0uozvHvZ$z?|E5@W1=t-Oex|^Yg`O42ioAWFI zXPyDgyfR^}`FTG=9JHKG1b?x2zli*M*Dq@jW^6f$wZwV;wjWTgT0M+qKZKX409SWU zIJvmOMn7}%44+luC3HXpX?|4;B7Fsq0tT)I?!1QgB9O*%6+X9jfff?Y-}18p{7n8A zLSE5qeq?(@P8db(HL^z}@++7ivQp$pdgGT{IW}KbKU@lTPcLK% z4#U`mUt-?2GZ;DNQ{*mGT=;-;LD~9wd9LC{njO`$*D}-G3TF%L&7TSG!V?)O=X{Hq z@izC1^g&=185c(gV$%{CAa?JAgc}6Wq_8qEAe?6?)*MaJE$?A zt0RAJ=6j*N@J5E*g8yZL^g*(dvkP(;j6mb|JurOoJf7XS9jg+)#nK($VCL$r=s$Kc zqG~sSx3Axy#QQ7$CzdeTlQOQ|+vD->q77)(V;I8nM3Cp*5Z!ffs(x8>{2u%n-^c%m zynNpBk>-;hTCSE*@-Bp9SZJ(hZPg#F+Z5~t`<%6)d4`(5C(is#3uiy%k7bT|PsuZD z@(aj<4tvEQ>oL!| zzr*`{$8l}a$F&)Eb3fP??u~+@cEG<>9cWpq=r{}B1=$C34QUQL&R0!hdSJ`<*NCeL z2d_TD!c9l8muCT=UuMFEIlzV41YXZB0uHZvhMKvf(VOEfw5^V_;4UCC(61D7@NCFE7w#xJe??jkm^cg73e?Ai(f4tc z+J9O&JK_)2cYh!4i~kFs?Y)J)*MN=3ufo?q0A9X+n3r%)$$+<~twgh)Bk>u}NT0ZT z7kvCP3+@7Y38EjqNRfR~YkjQ=>q(k-Udn`S#IrZtE5Cy}U))5A5sP8tO#LGtdBZ~c zzQmv7#U&#ewPp?%(p~s`-?c|5T&5DHHCyro-bO#{=#+@6w&PIMW-L0CUWkpSl6aQm z0XUHN>$K@cS@%fcKVb3-6kr}FLngL=djj)$D;{}KT}%2+<|li3V=gi;Eg_gRCtu3+ zn#diy5H*&cL6)dS&^&@U*3Dzypg(1YoxKC5EM0FI=lieziTs62U|!o#)5ZVFyN~Gu zu^d&ju?ni$jla(Q>enjSMAqEdj zHyZvDud?1)PPk8f5O&NJ=IrecThY5RkS~Pah5m*5kpBr8Bakg;D0ZE@rOP=_w`u>WDdmW1|MK-y(wr=v<0Gy)koC| zjZmX@D>RRZL+4(D(JFQbD%Ng`k5=tR_fa1rgRei9?7yb)U(0jQ^AxS1Xf5M@@;d6m z94CXOESI@PKL3s&Jp8gtbKdY#gP887;Y+?k$0?hUFOss~n{%bmU-3R^pEVM0?jBfo z=p4Sj@erGj-N4rGf8qONd_x}iX!$yX7cYT<Aw0-_~Y=;z!6>rz9t-E zpUqmc4USIE7|e6u*PVWZ(I0Q7JW%=DR3BvGxtz^Xdh7QENy6=XCG8+&=gCz9YTkgdYINX2%9E{r~?+HZGpZ&WjHUF zZV$~%&4CGQr|8WuyHw!V`g8QmW#}_!C%R7Cj)5N^K&@6?VdI{S@}K&y^nph<6e(8X zeq@chXj0T3-_P($!C(4-aOunXFqx8Z_U5zUZuHNAs}Hbd-$`seaf7n-G0H{NB)vJ_ z%TWJr_zusrQg#60{Z}FT$_oBd7q7v-#6OX6?!MCHV&eM3hV#6ZpE^;Vwy@#2>^ExS zJ#U|k=$c!@hWd6m*v}c?O?4;zJ#lKj55Ax4jfeNH;>w+WhQHAday|T$3Dp}l)A_y> z*W1-N_t-heT0?ZdS5b2ErnlR6?`INyM)dcM$1WpZVXpZymxHp2_!fe1=FC{J>o{D= zr;V~`7*#;S$7M9^i^-0g$Clv1uSaotLt`ZU{4ElHyo-a^9;eIy`>sBu{+s@H2L3gh zv=Q3^G{L`U6XI?oc7_740(st3bjoi$JToNES-m-V@5Q?)8Cg|d_fpSSYk#&SN1kvj z-uM+V`1qovw}y^E8V2Qcz{TC`Xt6datM#u(7UALV*D!zSO6vK#Zfs3kSr^5x_!gsA zev@wB==dk2Qk`fs{kcxdoYz=qi*52%7106JT$C$+L7ngOa*gN4@t=uvPp%fQ$y@@N z{IbBw$q^YHH8cp&&^fOMe!6%@;r`&aA8~5#5Zu1`8_HCuf*ogWri)|mnY)mw-UO^W z_FJm&kFal+Z8?HIqbH(hY3cw%|3VFDcXAf|hrKGqd0)|i_mm6>#P}NZkc)8?HnyC5 z+yW_sd5%^X$D-(dMf$_GaCg`TmWK^>puCSod~?k(av^IdEj6oXq=&ZBqtZU~Pk ziLb6DrNYnPgApsfhE{GM^0k_b1zhv&PfWtT3->W^-F^gy547R~s1s2~^$z73 z@wVtzr34m_8;tq0rXi|gdDv5*A2e|$^&5R{WZ-RF*B$*8&SiVUrdTI9MZAS<<(eXE z!HTeFJ|>y-0lWsZf+DSTamUa z4+Q1Rg%+LTur%Qa4yGgP_niL&^}3FPOJ?$xE9Y2yXJpM0g4t_!px>DHMAs9L^=<+| z)+VpIhz;{pvmoXPkK(zXjQ8KdSl^vBy5SViAP9VVfGiYQ(MXALnlGWLi>AawO|#9!no=VPEX7(-Y@=j>PSE z-@|hDLy^EB_5;rpch3U5>`}1K)c`hr1-WJ>kGT3kt27u|)Nt}iYuIGSj*tQ+uwc`E z^LXHx;Ce~=Li*$z+5`o@Cd^s0+06U2N7&l$;23xvv^TmS1K*sj6PQ=wtj_%<{&N2N zR_O=AR}FjP`hc;xIesGMwP=BYSprntm90J3Yym}}WsB7HcH*r?jf7Tp6#L?BeqI>9 zS%S&mTd;&<;{64m;n4L*#97u$$vAZFA;!&Ef`}5O4c<@Y{3m|Hue&s1%t7*a#;VA< zx9aRu;w|_a`H!R@4m9b5ABT*_PZK8M!zR(lnK7fjK6mosyiM5^5CJW61hmS8DE2`W z_C>Z5@Mes|{4e&RXqgI#Zrcqj5{_cSHidCsgIjePFFm0*wUX7hv{B7CJIv;7;Jhtam*gMk0 zT|N{1-*IHF>@NLh;?ve+EN7<)FNj>w^PMoy9rGN0#WSoA)b5Ovt$O3;@NxK^{jj!E zXVeQ1ho>{~k#UiUGG4|*#bJaYu<^`_=+=yjzWfJPY(Iu~=d9q|k(2uguEe_sw0h&A z)fq?ILpI@oM1uIzzm6h0Mju-T$D~|shM>{Tdx+ip8)m=%88+4HZO#G0SU#2SMaTaj zV>peRen;wd#Oc;?_;uJ=Je)EG#|OWS0ac>Z*sv41=tX>F&M(*$&hB2Ay*2@B_MB37 zbn3X&E4bFEO}qta#&h3=I-njSkmWTX@5X0LA>AJp?D;sU5q4z^U6+=qG$qnvFlF^q!$QRXd@b8Ek6{PK4VI^q=f@PB(d9uFr?#;@;Az{1w8 zP(CatT&PRgI8wLcd>@p%!2K-Qb0lldlo^G&&Z$0{c#oy57)l+3dXeb=dQKM6&tGNn z=FJ!RIk_hfE!!Af7oAYJ3;qT)N=QP?p(Kpi@B^0jo{AmSyYZaYPAZ01a`+r4?&{25 z#_&q)zQppLAe?Ivi=Sxod_eu-kN4ikp5A@YrCeEL@$}-pkHqWpjBdaDCXM=nR)A~1 zkn+^A{PeY-exCWOuyEMAog8Ht{t*^kstJ!##go8H}Ee>8l0>c5NcY*ydI+GD1C==Uix8v$4Q7; zdGU!pkiKZNCkb7@Ny4}t=dk{rc{tU)yE#@{a`m;xS;p!MoFklP{T%oB<-O!;l>A-S zVtV3M-$A%LdNi(&8I8mJ`r{MoRXcmeVSBe&(J5q~^Ht4#BIDaICh?axAw{q;s*9TI z4cz6bWIg-i%erwG7rz?KHr_%#^LVH$*X<8bXX^ugb{`!MJw*Ke8~9|}Cpg|FR-H*| zi%PaxB z*%}|tv+ozJZ_(!je|>K6icQ3IOV!Q_41LeIZsWYSxWjvhn{yZqH{V8G;$C~p1JvAn zAC8-v`Sj1QJ#G~C)@i|b=V+{`SPK)1S3vvF zd_1erA07^l5)XNgZKJ-g&a4p{z04y0ScWg(i(V&dT%oCsm#)(Y{@WDpMqlWkOW)|{ zz3Dj~cU9-ySF;ON$Be;<$ve>a%U@Az;~i94e+y+ly@ev5+(Mqkw-EfVUl1_kXZX+f z5dpJ*M&P@L;OybYcwSrPm9mk2j6{Oac_E=QL3sUD*f?arpdE3)8OGd(3(CBu_}v=! z?UZ$*>&qFra+SO`g0}$)T>q&2X3TB1*DAN=q1w!w-EttNk6eb%bC09ivTG>5^cUn_ z_$#u{{RNq(|BMVD-hk@|Kf<8_bsBv>pT_ot5M-_Os^@h+Liv(j$wk}Fa^~kScsVux zBJW}q{dGP_!Ci8E?V`RTXS%E0{={G9^*3_5bMBY3+K)Brg0)@7(sr={#b#ebwmGD| z;6L?8xP5Q~cEzdN=`o(xzGBk8S6%E$`_*Orvs11~-b#b-b-AZ#tYn~7-AsFx-%NB} z=B`lZSxauSwD?p1HS{0JUoAOn_O%>+t?{?U*?6yCQ_lA`pYO_iE$U2H^Euwa1D0HVXY-i<$U&o zt@>bom+{DvIfvjsmmsp}br*j=^lInO(rS#S<^gNGjrY3Dle2BC&$dRm)ks}W<>{n;bF6*~WO8v4`ggG4FK<@9 z*0WcOQqTXrUxA8Nb6;A#jk->Mk@@OuLpj^da=u;4IfmBs7yg$#HY#5+bJxf@g$DkI z>oh|KM<>y1s;9#L^;X1&vOF927}hg)+mkdG+>Kn%#9!n-b9$#b+mY)%L;e~0T#a)Z zC5N-{zT|1O#$EKhoz!{U?Cr%*yvXZC`H826@snTL<}9M-y7c=%`az%n)VxQYmotr? z$Yp9izswl>lD|yWfEOF_tTMBHE4T}+uUuF1nB3*FgP&;8>;GV&D|2K{Ego&g)8KjQ ztB$`q-%WHLGyX;nSB0}#<{7zL<;*4}_c-p;;{S2kYO^auB8}DTdNDebO(}Lrl`OB>5JVV|Y`ATJ-FK1NiO?R z-u(RBY|l@U!*6{hIR`AgJ}~nD^&kCA4;_Cc^Jwo%i@(S{m7_Tg{pDPasa)&uc|Y*k zuX8WsX^f&9hA_ENk4IVgY z&e<+wK;>zdx!;U`BCmtPi_6@fKzJ4FJKpxWi}XQ#!VOs$Pohmg_C0m_n{_~$1Bvr~ z%6l3AsbnB!zUAx!%0IzBt?nav$-9PylmBhL;B)>Ry8p9uP6U6n&)Lr2)Ho;{E0tXA zs^@nm{>J!!V(zCLNQ=Lv)LkHW;&*bW8!>c$Ou~@F+cqVn@YW^Okit2OY zN~aiA|EsG!U&@o$MERKQQQxP%vc2kat80?{p7^A?GJAq*m~rLzs=-!Q^}W^q)O}e` zFyAj;uKd4Tv>0z)!D)6UMK6#w)s|XCb3e@wZ2Moq%O^rt2{T= zHQroLFkchRSK_UH#~biU4{PHpgq3nz<&F?TdIX^z!ICE;l$BRkWQ+$B1`zTQ{!@#z zGwm3aHQ9%nz1v;Jc*{Ih`8gUN!2h%)j3$T<(VQSM;YEwIS9~AZaqnP0{lT`-wDu8y{p=uw>$U#367qR_?gT#&sxG= z+&%7f9`PaGS+oW3ElQbD)|?^i%fng_eXsK~=Eo8rPv0Dg zeU$j;pgQbt`uXMVzGqwhXP^vK!<6|HRr8_tBtrEb`KC zx=`s#sQFfBWDm_FeYBn+zA4XI9LdZ3G|As!X}7t5rH{kec4@yaef5Ir$EsrcdeJYZ zGkvz4FYqD0ghqr930nwXlRpHu^8IvP+wv+r$*?4wiEaCiRdH()pWeRzScK#)gw@CI zVm;&5Kl$cYgoQJP;-8JFS}Nu+ztLvta{;N#H4ZislkP>I;R5tMl{oxxLJrbOW1PK> z&1GNzOpG6Cu6(fPe!dH951hst#u&~1YzMjz9*Y7+O5S&HbdY|X<)0-x+S1p#VcQ<` z$*ZE`%jfU9!1gEO^OH%4;Ai|@MZ&X|x7q$=%`?ZJ%&B40@^vw{TjwLE(nH`=o;)LC zBAo4)xGIUQ^eYNK#tur1{Ei=f#lFi)_~yi)`2Ha9-Cp4MKKjlcF=5B4B>Ij0iAf9B zAUG_y%DwKz`1t`JEXMSW-=S2M`t&0XLz6CVqeP`T90#_t$0~Z*vzB)J;kShHguN2$ zY4p8qL?4u#b_D(OUmU-VhFmNEF!qim7WUe^dOT?V zA}cYf*A66He2h;IoQ0c*Cu%aT{d30twH-Jf(>5H#z{yKh%r~EZhwxA20e`M|Q>H&D zMJX@uY60}6;y4cOv;;L*Ux0%z{VOD=$XsWziPat?N?RX zfz!kPKqHSSsAl&LCd41WoGsrnKHZ&dKC1Y=X}2Nh`~JsjJT&b_|6kpQd#Gy3^Sy}7 z;GD{}1ar=0jcSL?BJsF3q$Dr$dpNz?s zwxY5&22q5L<(49T)@sJ|6vU^LJ?j}`Teey>iqSVg>I-&VgeT91<@5JFU(+rB64}?C zI3$;?(EwGN$HK-dr`~>vDGBcZ2l~!0-h4pyz4%1$y~vozB4x3l-KTh`!AFR#J_zk= z_d_k7Q{6N=4t49rp=H}~Z2w9WEL9N_GyFMaXh{BIaAXdOQ42RQZa9f?sPrY%w2OrF zWUr-xPkAGH{eh}YV@!SL?n1)>)7btHwwvQt`aWAV6s6DqzKb`p_=^N=*nbq?{rm`@ z?fVX`yZ0qtUo)nTF|ZO>#<3wWvH}wOx{CQCeEqXx+WMmyGII_6zU>vyNY0E>n?i|LL_8yh0&u}P0LW?!dncS`*L z{m$ooo`7keY{Z76ml)gU&U@FQ-#TTot-b77yr|8-V~KJVap20I*vr_;<=YRz!QG$F z;9Qn5Kic-_|EGTj4@@gdv&#$M99%sycik>{ySt%7u!gbW zHaNX_G?E|wfmr)epjpWUqvdD&1DThJYtI*H zxcR>sdHt0+T1@4t)PhYwImV}yB7YWUTw!VC%2yOF^wTcuqoGqS7u>pj38v)7xU_yc zzB_&t1K*vVs{PXDrlUW{fcX;3q0>Rdiypa(akH1um!15RE0Vgb+XFuLMR{LP$m8ei zdVgy5#;BS*4;-8`h@Qr0F#lKt<8zDjf-PgP+?g+@QbrBqTh-(^9)j<8Z9!CIB;H%F zhV4wvL!Ca{43~<7&|~VC_+;;SeDuk7ryAodeNm)TdF3UaZav&a3Jlv^dE^9U8o~U9unP__M&pf8uhj%GG;f4fA3cMx^3s4&WVnL@WY_t z=wCSsz8=JbIgxDgwILpTVH;GLF^HimUPa4W3r_wy(4<2gwlRh)CAX0YpB+4h@$*(- z*|u-+)unsL8YDhGJm2Bv$L+Y$Px;iOqV?GRSo(3%Uyrb}R(G6>9f4CrMxa}{^6+KM zq@82v>`xU7 z#C=jZFMdb0Zan9!3l27n!;OJsaC+!)bT3~H-mZ*iapPRaajI1qsK)^1t;-n6NXEP@ zMeYKH5tgqYGG?MI@Fdh3qw5Zuwv*4Ydr9p!Zj+5N;`>su6EJ4pKCEvx*xdi!=<6hY zF5KU%#XXHW9r0b$?)Zu0@pAlVeAJ{dA_4;$o9oT>GWE&QJz-0k(|7a-j9I>idT+I- zETc@W$njtIUDAtkS@+$RdFN%7!uIwKrweo&PoLx85xwgHy6?J)xf9o5bL}|p)5#tk z_vht2E!mSa$D*8U)(t;0PtEOdcj; z*Y}IRgofL4U!#3akBqlxmTaPY;y2dpj)_B-q1lGtQpH3_e9++SSFwD;5`5bt&T?j< z?1{;lX6jrsIm?jy;#XRC!*9Lb#xFxhVkc=bqGk=$FIW)K5fNxoC_>u*!AoenGY9LW z(tg<=u8L$_hmx1#*Do@s8&)+RhTgLdT4*704fW{H-(=4N^xyLfmd^eh$J#RvKDs^5 zv(0)81J7Gj?_-o-=jEn6bF*b9{MfA*e(K#H-*)Pb+PO&!O`Ge5za#SV+bFh5_oFYc^d>5- z`2{T(pF*c*{m`2E5vpbhGUfL4en_9XU--}jK3DAS*$K~EWQ`ExVrze^Pu@tjSNwsb zoou`C4&QI#XV!l43GDyflsOUuCcY8->vX}IuH!N6!<}fb>=H^WqwR0uZ^%09XL!>G z+;!3w*ySuM{IQ!*i%^gtw#FA-pYRr%C-sA?{kcz_;=7}_Uwr$e|7FfK{QBg4N_AE( z{n`wl1|yE|IOWg5=Djej|6Ej?d=%Ljzu^7RPjGwZ2T)duURhcm_>zkah0Vvk@|J(H z{fZ8Pi{5_alSX;JTl~_@eksZ?N#YRAK6!$}sroTE9o-q5+7H990ZS1z;8WN;x!xqd zNT0uoQY*h#K=P(kKK4et#ixdP0{JO{_^7tir%tus>`$!xie&yW`&7s|&>}xCMvJ`Z zfKAmJ$r*C8@2t;DD|^{!Yud<|ds)TZ(Z=?N_lwpgzZgD1sjqrG0OKC3^L%6B9R(5x z+d=sTDPM3apAh8>%eG6r+v-X+CEjr_8>{pC^49c*kA9i$mwxC~JYzQctfX#t%9s$p z?ruugkv8j);@9_$x&MWK#E<8q_%Jt)QNCRB%T^ITj+HN~v;X2gGLDarji_NY4y{~jXpR0@V~a=fAK{<&-pgd+;;Ipnp8~L0%cF*RhQ}kE}oA*VLYWSL#^8{V?g&Q z6`$f%{-kEU`Hp@61N9%7YpvTqRQI79{;C_#`wLG_EtxSpzO39a4GuDI2@mN$VPA3V z9+G}Hx83lC)yHj0`^5*aUq0p$*R;6T(0+M@CkNS{sLVNugW>CL__)S$-Zk?O+i&%8*g)_GriSUZHO98~RJQ~TvHvX8j0xNmmT$_nC}sq>NO4dRn2>y@uK z?;7nl&QTU$%v0naIWt>ir{RbBRu1k(XycH>bFeAAi!`!^~FgyN&(ji}poHc4!V zoNsRE4}yc?3uyR(iXXVdzaC|LYe8=x(H%+}H~lwUWzTl9y^ZaUp&l7c?V0OpGINuz zsnC-6#E_q27{gDUfqe5_z1AEfjqo1*wremqeFjHIIgj%!|DWT9+ubFSnWj3u$ZhEC zr_KSG&$;55i=D%r%r0J!^LqK2B7FQ!5#D~unO$6yobBvx@OL{2?+_{yB*)i_mv!9A z+BfjU@1x&A?5jQm@s+Dis7okM2qws$|I5&gd7JbLu!lZFGi5>y1Wry; z=T&ZkMDx|C$D8ZC$oC@fMd&bX@Y*$>9m zNq*0MP*>hxUHLq96WnksSOMc#QDsL0= z47U;<6aN6mXZUT}pJb!Gy&d<-M(}yV_+=GB7tJI10rTcc_lKqZLz;RkR}Zg~1xr=F zPkXbB*C7OH&(n$r_rqsuSt{Mv!dlY4*g^L%atnM!f1_lzx9?lv5pNRkH#Z6|9#!Yz z)XxuN^xSo*->z3O^KeU>shIz5vD4hMZPxPAE^9eIfqQx4gOaf*_X4`|bH4Y^^BeCX zGPfBap1v{@2ygR!O@ce$w;VD}avncs-rwfvJ$@b*?oLFhsCv@wzoT1OElWlEED&$y zizI$AJ()|dz1gRP`?~UcN6!cJ54qcI&}htEy9*n>y^Jr8T*B1ln-JZm%WWqYmo3?H z=0%;iIw7)lGqi{sNq?Q2s1n^yhRff{`KmlOWjAf)>(%)%+{bjOFan{iIbw3fa36w;kQQn2#u2@L%;e&r9=_&D7Wli_vYy4g^P#CpiW@GUrWh zP`@e;Pt)^4U*6ITH}0k0e>C>-=6|^t?RyPEFXj)^veSN>x2il}#>+Dmj-jcGHtZ*E ze;{WBeSCO6P3`7w9v9E#j1{%)aB5ab`T;b;Z#REZ=WZC!J!rib$G(_2e>L};3vtdX zCcGc|Ob^$8)FyvKF;fl6)ti-V=U!mRsD{Yw?ud>d4!CpUGH#woz}nB(V&A0)djIoz zF&}KEf7e4*-+lgf^cXgddC~IIC(`RUZ~H9r&uC(gLB*obrA&FA72?5h*96;X8>W9jxI#8>Bo)rYRazX<&Z-270u zR0Wi-#QlE#3~(9O|D@Pwck@2Bp>k*1_qyR|+ksf!rUOca=7nwMQn1a}0`81o(LyW3 zE_W5wYtw^g7Tn<;rirlk<*E+vj;W|6}9o z;B&N|` z8pD}m*yRH6^-pyGx(nFH=j}Kuxmm(JOq{h78yXGZ^E<1xsq964*QhhD#tp*P%=^`o z{s`Wg!(i*39qoDzdF-5-GBb*Pauq1I?47Ui`Tr#EjZ+3kmpMZsYM2I3-oboKe?az> zVrciDF?k(!HSMGFvB@5itjB(c?u4tI`{7FeA(+{uDI$4(g^PbK?iGhI@1U!U`x=C2 zEde3!K09WX()WdAUu9m`DQLUqvc50GoN+O`Ze#A;EjYrxhYMVX%RY+O6wb?waO;!ai9quHn9p*yHt_HL?%e4V!AkVd98YXtw@mm7A&> za}d?sdoD7}t0$v8{SErj@LS);aS&&g$wWHp!@GKwRdem@gsuABYgmMTZq1|qVH97S9UeokC2>vVpo$G)noNL;)@RP=o-HPVUsVw zwcQ+%Grb7URcHg4wov5{5?hSewsrfq*u*4P$Zp#7_R%II_V~lJ2S|ME_q4|d-On`b zj9w+x{8mkq+#jntKP%prw(<`}qqB!UPHUf*=ga=D%F&_hLd?y=Hi%72Y@rv-cHBPs z3Jd;R8?Y>R=4n>_-27t0ykTn=mu+FOM@cS3dA{_$*kr{fB{AZ%7a{hXGqmLxHsqMF z{G!`FE1z3G*ZO^M5ud9hkU_4Myx>O@c!2@J24yH+#(Kc??9zL=FCg|fv4pQJc!T7($4!5{XXSOQUvL(2Lni-Rne$-F-lI^hNr#L4zqQ^WbaLal z54(b+TB|ix%-io_`oA_}@4&MdUvCv++s?=ES!>aXu`gw7M9X^ZiBh^|(+!o|4~Jb? z6VrtFmN>qBD0ZFtQR(~#nD@NnfYAslPz%2gunX6*#W6j5*TX6Ka?veC6=STJR354cDAC(WGW|=IfiQ{@-xm zY!s_h7rp^m&Hegh3;dnkYc46*3fmiXM;y;V^2)*-26<|uLC>)~=kOG!EM5nDXSYpH z{!{;idSQ;b9mm}LwDoBE2z0^MExKWN_4-^V))LqgGR?Q##^~95u&?WIyTo{WR91;p0B7= z9HlIkJt>*T>ILQ)lInr!SF$$FUU1(k?i)$IE}4(?J;in?nK4uM^#Ah2r*@tW&W-8@ z=9}9scd?zl@)llK#MeKbGA-K^zmyLsD--sJp(_Ov`96W8DbWg~u6FP_>f;3>`5Qta HCB^>%Zp1Qi literal 0 HcmV?d00001 diff --git a/data/yatta.png b/data/yatta.png new file mode 100644 index 0000000000000000000000000000000000000000..4f230c86c727f4950a5c6e6b62b708b4d3d12f8b GIT binary patch literal 34873 zcmV)&K#aeMP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00006 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 literal 0 HcmV?d00001 diff --git a/setup-ahitclient.py b/setup-ahitclient.py new file mode 100644 index 0000000000..18fd6a1887 --- /dev/null +++ b/setup-ahitclient.py @@ -0,0 +1,642 @@ +import base64 +import datetime +import os +import platform +import shutil +import sys +import sysconfig +import typing +import warnings +import zipfile +import urllib.request +import io +import json +import threading +import subprocess + +from collections.abc import Iterable +from hashlib import sha3_512 +from pathlib import Path + + +# This is a bit jank. We need cx-Freeze to be able to run anything from this script, so install it +try: + requirement = 'cx-Freeze>=6.15.2' + import pkg_resources + try: + pkg_resources.require(requirement) + install_cx_freeze = False + except pkg_resources.ResolutionError: + install_cx_freeze = True +except ImportError: + install_cx_freeze = True + pkg_resources = None # type: ignore [assignment] + +if install_cx_freeze: + # check if pip is available + try: + import pip # noqa: F401 + except ImportError: + raise RuntimeError("pip not available. Please install pip.") + # install and import cx_freeze + if '--yes' not in sys.argv and '-y' not in sys.argv: + input(f'Requirement {requirement} is not satisfied, press enter to install it') + subprocess.call([sys.executable, '-m', 'pip', 'install', requirement, '--upgrade']) + import pkg_resources + +import cx_Freeze + +# .build only exists if cx-Freeze is the right version, so we have to update/install that first before this line +import setuptools.command.build + +if __name__ == "__main__": + # need to run this early to import from Utils and Launcher + # TODO: move stuff to not require this + import ModuleUpdate + ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv) + ModuleUpdate.update_ran = False # restore for later + +from worlds.LauncherComponents import components, icon_paths +from Utils import version_tuple, is_windows, is_linux +from Cython.Build import cythonize + + +# On Python < 3.10 LogicMixin is not currently supported. +non_apworlds: set = { + "A Link to the Past", + "Adventure", + "ArchipIDLE", + "Archipelago", + "ChecksFinder", + "Clique", + "DLCQuest", + "Final Fantasy", + "Hylics 2", + "Kingdom Hearts 2", + "Lufia II Ancient Cave", + "Meritous", + "Ocarina of Time", + "Overcooked! 2", + "Raft", + "Secret of Evermore", + "Slay the Spire", + "Starcraft 2 Wings of Liberty", + "Sudoku", + "Super Mario 64", + "VVVVVV", + "Wargroove", + "Zillion", +} + +# LogicMixin is broken before 3.10 import revamp +if sys.version_info < (3,10): + non_apworlds.add("Hollow Knight") + +def download_SNI(): + print("Updating SNI") + machine_to_go = { + "x86_64": "amd64", + "aarch64": "arm64", + "armv7l": "arm" + } + platform_name = platform.system().lower() + machine_name = platform.machine().lower() + # force amd64 on macos until we have universal2 sni, otherwise resolve to GOARCH + machine_name = "amd64" if platform_name == "darwin" else machine_to_go.get(machine_name, machine_name) + with urllib.request.urlopen("https://api.github.com/repos/alttpo/sni/releases/latest") as request: + data = json.load(request) + files = data["assets"] + + source_url = None + + for file in files: + download_url: str = file["browser_download_url"] + machine_match = download_url.rsplit("-", 1)[1].split(".", 1)[0] == machine_name + if platform_name in download_url and machine_match: + # prefer "many" builds + if "many" in download_url: + source_url = download_url + break + source_url = download_url + + if source_url and source_url.endswith(".zip"): + with urllib.request.urlopen(source_url) as download: + with zipfile.ZipFile(io.BytesIO(download.read()), "r") as zf: + for member in zf.infolist(): + zf.extract(member, path="SNI") + print(f"Downloaded SNI from {source_url}") + + elif source_url and (source_url.endswith(".tar.xz") or source_url.endswith(".tar.gz")): + import tarfile + mode = "r:xz" if source_url.endswith(".tar.xz") else "r:gz" + with urllib.request.urlopen(source_url) as download: + sni_dir = None + with tarfile.open(fileobj=io.BytesIO(download.read()), mode=mode) as tf: + for member in tf.getmembers(): + if member.name.startswith("/") or "../" in member.name: + raise ValueError(f"Unexpected file '{member.name}' in {source_url}") + elif member.isdir() and not sni_dir: + sni_dir = member.name + elif member.isfile() and not sni_dir or not member.name.startswith(sni_dir): + raise ValueError(f"Expected folder before '{member.name}' in {source_url}") + elif member.isfile() and sni_dir: + tf.extract(member) + # sadly SNI is in its own folder on non-windows, so we need to rename + shutil.rmtree("SNI", True) + os.rename(sni_dir, "SNI") + print(f"Downloaded SNI from {source_url}") + + elif source_url: + print(f"Don't know how to extract SNI from {source_url}") + + else: + print(f"No SNI found for system spec {platform_name} {machine_name}") + + +signtool: typing.Optional[str] +if os.path.exists("X:/pw.txt"): + print("Using signtool") + with open("X:/pw.txt", encoding="utf-8-sig") as f: + pw = f.read() + signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \ + r'" /fd sha256 /tr http://timestamp.digicert.com/ ' +else: + signtool = None + + +build_platform = sysconfig.get_platform() +arch_folder = "exe.{platform}-{version}".format(platform=build_platform, + version=sysconfig.get_python_version()) +buildfolder = Path("build", arch_folder) +build_arch = build_platform.split('-')[-1] if '-' in build_platform else platform.machine() + + +# see Launcher.py on how to add scripts to setup.py +def resolve_icon(icon_name: str): + base_path = icon_paths[icon_name] + if is_windows: + path, extension = os.path.splitext(base_path) + ico_file = path + ".ico" + assert os.path.exists(ico_file), f"ico counterpart of {base_path} should exist." + return ico_file + else: + return base_path + + +exes = [ + cx_Freeze.Executable( + script=f"{c.script_name}.py", + target_name="ArchipelagoAHITClient.exe", + #target_name=c.frozen_name + (".exe" if is_windows else ""), + icon=resolve_icon(c.icon), + base="Win32GUI" if is_windows and not c.cli else None + ) for c in components if c.script_name and c.frozen_name and "AHITClient" in c.script_name +] + +#if is_windows: +if False: + # create a duplicate Launcher for Windows, which has a working stdout/stderr, for debugging and --help + c = next(component for component in components if component.script_name == "Launcher") + exes.append(cx_Freeze.Executable( + script=f"{c.script_name}.py", + target_name=f"{c.frozen_name}(DEBUG).exe", + icon=resolve_icon(c.icon), + )) + +extra_data = ["LICENSE", "data", "EnemizerCLI", "SNI"] +extra_libs = ["libssl.so", "libcrypto.so"] if is_linux else [] + + +def remove_sprites_from_folder(folder): + for file in os.listdir(folder): + if file != ".gitignore": + os.remove(folder / file) + + +def _threaded_hash(filepath): + hasher = sha3_512() + hasher.update(open(filepath, "rb").read()) + return base64.b85encode(hasher.digest()).decode() + + +# cx_Freeze's build command runs other commands. Override to accept --yes and store that. +class BuildCommand(setuptools.command.build.build): + user_options = [ + ('yes', 'y', 'Answer "yes" to all questions.'), + ] + yes: bool + last_yes: bool = False # used by sub commands of build + + def initialize_options(self): + super().initialize_options() + type(self).last_yes = self.yes = False + + def finalize_options(self): + super().finalize_options() + type(self).last_yes = self.yes + + +# Override cx_Freeze's build_exe command for pre and post build steps +class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE): + user_options = cx_Freeze.command.build_exe.BuildEXE.user_options + [ + ('yes', 'y', 'Answer "yes" to all questions.'), + ('extra-data=', None, 'Additional files to add.'), + ] + yes: bool + extra_data: Iterable # [any] not available in 3.8 + extra_libs: Iterable # work around broken include_files + + buildfolder: Path + libfolder: Path + library: Path + buildtime: datetime.datetime + + def initialize_options(self): + super().initialize_options() + self.yes = BuildCommand.last_yes + self.extra_data = [] + self.extra_libs = [] + + def finalize_options(self): + super().finalize_options() + self.buildfolder = self.build_exe + self.libfolder = Path(self.buildfolder, "lib") + self.library = Path(self.libfolder, "library.zip") + + def installfile(self, path, subpath=None, keep_content: bool = False): + folder = self.buildfolder + if subpath: + folder /= subpath + print('copying', path, '->', folder) + if path.is_dir(): + folder /= path.name + if folder.is_dir() and not keep_content: + shutil.rmtree(folder) + shutil.copytree(path, folder, dirs_exist_ok=True) + elif path.is_file(): + shutil.copy(path, folder) + else: + print('Warning,', path, 'not found') + + def create_manifest(self, create_hashes=False): + # Since the setup is now split into components and the manifest is not, + # it makes most sense to just remove the hashes for now. Not aware of anyone using them. + hashes = {} + manifestpath = os.path.join(self.buildfolder, "manifest.json") + if create_hashes: + from concurrent.futures import ThreadPoolExecutor + pool = ThreadPoolExecutor() + for dirpath, dirnames, filenames in os.walk(self.buildfolder): + for filename in filenames: + path = os.path.join(dirpath, filename) + hashes[os.path.relpath(path, start=self.buildfolder)] = pool.submit(_threaded_hash, path) + + import json + manifest = { + "buildtime": self.buildtime.isoformat(sep=" ", timespec="seconds"), + "hashes": {path: hash.result() for path, hash in hashes.items()}, + "version": version_tuple} + + json.dump(manifest, open(manifestpath, "wt"), indent=4) + print("Created Manifest") + + def run(self): + # start downloading sni asap + sni_thread = threading.Thread(target=download_SNI, name="SNI Downloader") + sni_thread.start() + + # pre-build steps + print(f"Outputting to: {self.buildfolder}") + os.makedirs(self.buildfolder, exist_ok=True) + import ModuleUpdate + ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt")) + ModuleUpdate.update(yes=self.yes) + + # auto-build cython modules + build_ext = self.distribution.get_command_obj("build_ext") + build_ext.inplace = False + self.run_command("build_ext") + # find remains of previous in-place builds, try to delete and warn otherwise + for path in build_ext.get_outputs(): + parts = os.path.split(path)[-1].split(".") + pattern = parts[0] + ".*." + parts[-1] + for match in Path().glob(pattern): + try: + match.unlink() + print(f"Removed {match}") + except Exception as ex: + warnings.warn(f"Could not delete old build output: {match}\n" + f"{ex}\nPlease close all AP instances and delete manually.") + + # regular cx build + self.buildtime = datetime.datetime.utcnow() + super().run() + + # manually copy built modules to lib folder. cx_Freeze does not know they exist. + for src in build_ext.get_outputs(): + print(f"copying {src} -> {self.libfolder}") + shutil.copy(src, self.libfolder, follow_symlinks=False) + + # need to finish download before copying + sni_thread.join() + + # include_files seems to not be done automatically. implement here + for src, dst in self.include_files: + print(f"copying {src} -> {self.buildfolder / dst}") + shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False) + + # now that include_files is completely broken, run find_libs here + for src, dst in find_libs(*self.extra_libs): + print(f"copying {src} -> {self.buildfolder / dst}") + shutil.copyfile(src, self.buildfolder / dst, follow_symlinks=False) + + # post build steps + if is_windows: # kivy_deps is win32 only, linux picks them up automatically + from kivy_deps import sdl2, glew + for folder in sdl2.dep_bins + glew.dep_bins: + shutil.copytree(folder, self.libfolder, dirs_exist_ok=True) + print(f"copying {folder} -> {self.libfolder}") + + for data in self.extra_data: + self.installfile(Path(data)) + + # kivi data files + import kivy + shutil.copytree(os.path.join(os.path.dirname(kivy.__file__), "data"), + self.buildfolder / "data", + dirs_exist_ok=True) + + os.makedirs(self.buildfolder / "Players" / "Templates", exist_ok=True) + from Options import generate_yaml_templates + from worlds.AutoWorld import AutoWorldRegister + assert not non_apworlds - set(AutoWorldRegister.world_types), \ + f"Unknown world {non_apworlds - set(AutoWorldRegister.world_types)} designated for .apworld" + folders_to_remove: typing.List[str] = [] + generate_yaml_templates(self.buildfolder / "Players" / "Templates", False) + for worldname, worldtype in AutoWorldRegister.world_types.items(): + if worldname not in non_apworlds: + file_name = os.path.split(os.path.dirname(worldtype.__file__))[1] + world_directory = self.libfolder / "worlds" / file_name + # this method creates an apworld that cannot be moved to a different OS or minor python version, + # which should be ok + with zipfile.ZipFile(self.libfolder / "worlds" / (file_name + ".apworld"), "x", zipfile.ZIP_DEFLATED, + compresslevel=9) as zf: + for path in world_directory.rglob("*.*"): + relative_path = os.path.join(*path.parts[path.parts.index("worlds")+1:]) + zf.write(path, relative_path) + folders_to_remove.append(file_name) + shutil.rmtree(world_directory) + shutil.copyfile("meta.yaml", self.buildfolder / "Players" / "Templates" / "meta.yaml") + # TODO: fix LttP options one day + shutil.copyfile("playerSettings.yaml", self.buildfolder / "Players" / "Templates" / "A Link to the Past.yaml") + try: + from maseya import z3pr + except ImportError: + print("Maseya Palette Shuffle not found, skipping data files.") + else: + # maseya Palette Shuffle exists and needs its data files + print("Maseya Palette Shuffle found, including data files...") + file = z3pr.__file__ + self.installfile(Path(os.path.dirname(file)) / "data", keep_content=True) + + if signtool: + for exe in self.distribution.executables: + print(f"Signing {exe.target_name}") + os.system(signtool + os.path.join(self.buildfolder, exe.target_name)) + print("Signing SNI") + os.system(signtool + os.path.join(self.buildfolder, "SNI", "SNI.exe")) + print("Signing OoT Utils") + for exe_path in (("Compress", "Compress.exe"), ("Decompress", "Decompress.exe")): + os.system(signtool + os.path.join(self.buildfolder, "lib", "worlds", "oot", "data", *exe_path)) + + remove_sprites_from_folder(self.buildfolder / "data" / "sprites" / "alttpr") + + self.create_manifest() + + if is_windows: + # Inno setup stuff + with open("setup.ini", "w") as f: + min_supported_windows = "6.2.9200" if sys.version_info > (3, 9) else "6.0.6000" + f.write(f"[Data]\nsource_path={self.buildfolder}\nmin_windows={min_supported_windows}\n") + with open("installdelete.iss", "w") as f: + f.writelines("Type: filesandordirs; Name: \"{app}\\lib\\worlds\\"+world_directory+"\"\n" + for world_directory in folders_to_remove) + else: + # make sure extra programs are executable + enemizer_exe = self.buildfolder / 'EnemizerCLI/EnemizerCLI.Core' + sni_exe = self.buildfolder / 'SNI/sni' + extra_exes = (enemizer_exe, sni_exe) + for extra_exe in extra_exes: + if extra_exe.is_file(): + extra_exe.chmod(0o755) + + +class AppImageCommand(setuptools.Command): + description = "build an app image from build output" + user_options = [ + ("build-folder=", None, "Folder to convert to AppImage."), + ("dist-file=", None, "AppImage output file."), + ("app-dir=", None, "Folder to use for packaging."), + ("app-icon=", None, "The icon to use for the AppImage."), + ("app-exec=", None, "The application to run inside the image."), + ("yes", "y", 'Answer "yes" to all questions.'), + ] + build_folder: typing.Optional[Path] + dist_file: typing.Optional[Path] + app_dir: typing.Optional[Path] + app_name: str + app_exec: typing.Optional[Path] + app_icon: typing.Optional[Path] # source file + app_id: str # lower case name, used for icon and .desktop + yes: bool + + def write_desktop(self): + assert self.app_dir, "Invalid app_dir" + desktop_filename = self.app_dir / f"{self.app_id}.desktop" + with open(desktop_filename, 'w', encoding="utf-8") as f: + f.write("\n".join(( + "[Desktop Entry]", + f'Name={self.app_name}', + f'Exec={self.app_exec}', + "Type=Application", + "Categories=Game", + f'Icon={self.app_id}', + '' + ))) + desktop_filename.chmod(0o755) + + def write_launcher(self, default_exe: Path): + assert self.app_dir, "Invalid app_dir" + launcher_filename = self.app_dir / "AppRun" + with open(launcher_filename, 'w', encoding="utf-8") as f: + f.write(f"""#!/bin/sh +exe="{default_exe}" +match="${{1#--executable=}}" +if [ "${{#match}}" -lt "${{#1}}" ]; then + exe="$match" + shift +elif [ "$1" = "-executable" ] || [ "$1" = "--executable" ]; then + exe="$2" + shift; shift +fi +tmp="${{exe#*/}}" +if [ ! "${{#tmp}}" -lt "${{#exe}}" ]; then + exe="{default_exe.parent}/$exe" +fi +export LD_LIBRARY_PATH="$LD_LIBRARY_PATH:$APPDIR/{default_exe.parent}/lib" +$APPDIR/$exe "$@" +""") + launcher_filename.chmod(0o755) + + def install_icon(self, src: Path, name: typing.Optional[str] = None, symlink: typing.Optional[Path] = None): + assert self.app_dir, "Invalid app_dir" + try: + from PIL import Image + except ModuleNotFoundError: + if not self.yes: + input("Requirement PIL is not satisfied, press enter to install it") + subprocess.call([sys.executable, '-m', 'pip', 'install', 'Pillow', '--upgrade']) + from PIL import Image + im = Image.open(src) + res, _ = im.size + + if not name: + name = src.stem + ext = src.suffix + dest_dir = Path(self.app_dir / f'usr/share/icons/hicolor/{res}x{res}/apps') + dest_dir.mkdir(parents=True, exist_ok=True) + dest_file = dest_dir / f'{name}{ext}' + shutil.copy(src, dest_file) + if symlink: + symlink.symlink_to(dest_file.relative_to(symlink.parent)) + + def initialize_options(self): + self.build_folder = None + self.app_dir = None + self.app_name = self.distribution.metadata.name + self.app_icon = self.distribution.executables[0].icon + self.app_exec = Path('opt/{app_name}/{exe}'.format( + app_name=self.distribution.metadata.name, exe=self.distribution.executables[0].target_name + )) + self.dist_file = Path("dist", "{app_name}_{app_version}_{platform}.AppImage".format( + app_name=self.distribution.metadata.name, app_version=self.distribution.metadata.version, + platform=sysconfig.get_platform() + )) + self.yes = False + + def finalize_options(self): + if not self.app_dir: + self.app_dir = self.build_folder.parent / "AppDir" + self.app_id = self.app_name.lower() + + def run(self): + self.dist_file.parent.mkdir(parents=True, exist_ok=True) + if self.app_dir.is_dir(): + shutil.rmtree(self.app_dir) + self.app_dir.mkdir(parents=True) + opt_dir = self.app_dir / "opt" / self.distribution.metadata.name + shutil.copytree(self.build_folder, opt_dir) + root_icon = self.app_dir / f'{self.app_id}{self.app_icon.suffix}' + self.install_icon(self.app_icon, self.app_id, symlink=root_icon) + shutil.copy(root_icon, self.app_dir / '.DirIcon') + self.write_desktop() + self.write_launcher(self.app_exec) + print(f'{self.app_dir} -> {self.dist_file}') + subprocess.call(f'ARCH={build_arch} ./appimagetool -n "{self.app_dir}" "{self.dist_file}"', shell=True) + + +def find_libs(*args: str) -> typing.Sequence[typing.Tuple[str, str]]: + """Try to find system libraries to be included.""" + if not args: + return [] + + arch = build_arch.replace('_', '-') + libc = 'libc6' # we currently don't support musl + + def parse(line): + lib, path = line.strip().split(' => ') + lib, typ = lib.split(' ', 1) + for test_arch in ('x86-64', 'i386', 'aarch64'): + if test_arch in typ: + lib_arch = test_arch + break + else: + lib_arch = '' + for test_libc in ('libc6',): + if test_libc in typ: + lib_libc = test_libc + break + else: + lib_libc = '' + return (lib, lib_arch, lib_libc), path + + if not hasattr(find_libs, "cache"): + ldconfig = shutil.which("ldconfig") + assert ldconfig, "Make sure ldconfig is in PATH" + data = subprocess.run([ldconfig, "-p"], capture_output=True, text=True).stdout.split("\n")[1:] + find_libs.cache = { # type: ignore [attr-defined] + k: v for k, v in (parse(line) for line in data if "=>" in line) + } + + def find_lib(lib, arch, libc): + for k, v in find_libs.cache.items(): + if k == (lib, arch, libc): + return v + for k, v, in find_libs.cache.items(): + if k[0].startswith(lib) and k[1] == arch and k[2] == libc: + return v + return None + + res = [] + for arg in args: + # try exact match, empty libc, empty arch, empty arch and libc + file = find_lib(arg, arch, libc) + file = file or find_lib(arg, arch, '') + file = file or find_lib(arg, '', libc) + file = file or find_lib(arg, '', '') + # resolve symlinks + for n in range(0, 5): + res.append((file, os.path.join('lib', os.path.basename(file)))) + if not os.path.islink(file): + break + dirname = os.path.dirname(file) + file = os.readlink(file) + if not os.path.isabs(file): + file = os.path.join(dirname, file) + return res + + +cx_Freeze.setup( + name="Archipelago", + version=f"{version_tuple.major}.{version_tuple.minor}.{version_tuple.build}", + description="Archipelago", + executables=exes, + ext_modules=cythonize("_speedups.pyx"), + options={ + "build_exe": { + "packages": ["worlds", "kivy", "cymem", "websockets"], + "includes": [], + "excludes": ["numpy", "Cython", "PySide2", "PIL", + "pandas"], + "zip_include_packages": ["*"], + "zip_exclude_packages": ["worlds", "sc2"], + "include_files": [], # broken in cx 6.14.0, we use more special sauce now + "include_msvcr": False, + "replace_paths": ["*."], + "optimize": 1, + "build_exe": buildfolder, + "extra_data": extra_data, + "extra_libs": extra_libs, + "bin_includes": ["libffi.so", "libcrypt.so"] if is_linux else [] + }, + "bdist_appimage": { + "build_folder": buildfolder, + }, + }, + # override commands to get custom stuff in + cmdclass={ + "build": BuildCommand, + "build_exe": BuildExeCommand, + "bdist_appimage": AppImageCommand, + }, +) diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py new file mode 100644 index 0000000000..f51d4948ee --- /dev/null +++ b/worlds/ahit/DeathWishLocations.py @@ -0,0 +1,262 @@ +from .Types import HatInTimeLocation, HatInTimeItem +from .Regions import connect_regions, create_region +from BaseClasses import Region, LocationProgressType, ItemClassification +from worlds.generic.Rules import add_rule +from worlds.AutoWorld import World +from typing import List +from .Locations import death_wishes + + +dw_prereqs = { + "So You're Back From Outer Space": ["Beat the Heat"], + "Snatcher's Hit List": ["Beat the Heat"], + "Snatcher Coins in Mafia Town": ["So You're Back From Outer Space"], + "Rift Collapse: Mafia of Cooks": ["So You're Back From Outer Space"], + "Collect-a-thon": ["So You're Back From Outer Space"], + "She Speedran from Outer Space": ["Rift Collapse: Mafia of Cooks"], + "Mafia's Jumps": ["She Speedran from Outer Space"], + "Vault Codes in the Wind": ["Collect-a-thon", "She Speedran from Outer Space"], + "Encore! Encore!": ["Collect-a-thon"], + + "Security Breach": ["Beat the Heat"], + "Rift Collapse: Dead Bird Studio": ["Security Breach"], + "The Great Big Hootenanny": ["Security Breach"], + "10 Seconds until Self-Destruct": ["The Great Big Hootenanny"], + "Killing Two Birds": ["Rift Collapse: Dead Bird Studio", "10 Seconds until Self-Destruct"], + "Community Rift: Rhythm Jump Studio": ["10 Seconds until Self-Destruct"], + "Snatcher Coins in Battle of the Birds": ["The Great Big Hootenanny"], + "Zero Jumps": ["Rift Collapse: Dead Bird Studio"], + "Snatcher Coins in Nyakuza Metro": ["Killing Two Birds"], + + "Speedrun Well": ["Beat the Heat"], + "Rift Collapse: Sleepy Subcon": ["Speedrun Well"], + "Boss Rush": ["Speedrun Well"], + "Quality Time with Snatcher": ["Rift Collapse: Sleepy Subcon"], + "Breaching the Contract": ["Boss Rush", "Quality Time with Snatcher"], + "Community Rift: Twilight Travels": ["Quality Time with Snatcher"], + "Snatcher Coins in Subcon Forest": ["Rift Collapse: Sleepy Subcon"], + + "Bird Sanctuary": ["Beat the Heat"], + "Snatcher Coins in Alpine Skyline": ["Bird Sanctuary"], + "Wound-Up Windmill": ["Bird Sanctuary"], + "Rift Collapse: Alpine Skyline": ["Bird Sanctuary"], + "Camera Tourist": ["Rift Collapse: Alpine Skyline"], + "Community Rift: The Mountain Rift": ["Rift Collapse: Alpine Skyline"], + "The Illness has Speedrun": ["Rift Collapse: Alpine Skyline", "Wound-Up Windmill"], + + "The Mustache Gauntlet": ["Wound-Up Windmill"], + "No More Bad Guys": ["The Mustache Gauntlet"], + "Seal the Deal": ["Encore! Encore!", "Killing Two Birds", + "Breaching the Contract", "No More Bad Guys"], + + "Rift Collapse: Deep Sea": ["Rift Collapse: Mafia of Cooks", "Rift Collapse: Dead Bird Studio", + "Rift Collapse: Sleepy Subcon", "Rift Collapse: Alpine Skyline"], + + "Cruisin' for a Bruisin'": ["Rift Collapse: Deep Sea"], +} + +dw_candles = [ + "Snatcher's Hit List", + "Zero Jumps", + "Camera Tourist", + "Snatcher Coins in Mafia Town", + "Snatcher Coins in Battle of the Birds", + "Snatcher Coins in Subcon Forest", + "Snatcher Coins in Alpine Skyline", + "Snatcher Coins in Nyakuza Metro", +] + +annoying_dws = [ + "Vault Codes in the Wind", + "Boss Rush", + "Camera Tourist", + "The Mustache Gauntlet", + "Rift Collapse: Deep Sea", + "Cruisin' for a Bruisin'", + "Seal the Deal", # Non-excluded if goal +] + +# includes the above as well +annoying_bonuses = [ + "So You're Back From Outer Space", + "Encore! Encore!", + "Snatcher's Hit List", + "10 Seconds until Self-Destruct", + "Killing Two Birds", + "Zero Jumps", + "Bird Sanctuary", + "Wound-Up Windmill", + "Seal the Deal", +] + +dw_classes = { + "Beat the Heat": "Hat_SnatcherContract_DeathWish_HeatingUpHarder", + "So You're Back From Outer Space": "Hat_SnatcherContract_DeathWish_BackFromSpace", + "Snatcher's Hit List": "Hat_SnatcherContract_DeathWish_KillEverybody", + "Collect-a-thon": "Hat_SnatcherContract_DeathWish_PonFrenzy", + "Rift Collapse: Mafia of Cooks": "Hat_SnatcherContract_DeathWish_RiftCollapse_MafiaTown", + "Encore! Encore!": "Hat_SnatcherContract_DeathWish_MafiaBossEX", + "She Speedran from Outer Space": "Hat_SnatcherContract_DeathWish_Speedrun_MafiaAlien", + "Mafia's Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses_MafiaAlien", + "Vault Codes in the Wind": "Hat_SnatcherContract_DeathWish_MovingVault", + "Snatcher Coins in Mafia Town": "Hat_SnatcherContract_DeathWish_Tokens_MafiaTown", + + "Security Breach": "Hat_SnatcherContract_DeathWish_DeadBirdStudioMoreGuards", + "The Great Big Hootenanny": "Hat_SnatcherContract_DeathWish_DifficultParade", + "Rift Collapse: Dead Bird Studio": "Hat_SnatcherContract_DeathWish_RiftCollapse_Birds", + "10 Seconds until Self-Destruct": "Hat_SnatcherContract_DeathWish_TrainRushShortTime", + "Killing Two Birds": "Hat_SnatcherContract_DeathWish_BirdBossEX", + "Snatcher Coins in Battle of the Birds": "Hat_SnatcherContract_DeathWish_Tokens_Birds", + "Zero Jumps": "Hat_SnatcherContract_DeathWish_NoAPresses", + + "Speedrun Well": "Hat_SnatcherContract_DeathWish_Speedrun_SubWell", + "Rift Collapse: Sleepy Subcon": "Hat_SnatcherContract_DeathWish_RiftCollapse_Subcon", + "Boss Rush": "Hat_SnatcherContract_DeathWish_BossRush", + "Quality Time with Snatcher": "Hat_SnatcherContract_DeathWish_SurvivalOfTheFittest", + "Breaching the Contract": "Hat_SnatcherContract_DeathWish_SnatcherEX", + "Snatcher Coins in Subcon Forest": "Hat_SnatcherContract_DeathWish_Tokens_Subcon", + + "Bird Sanctuary": "Hat_SnatcherContract_DeathWish_NiceBirdhouse", + "Rift Collapse: Alpine Skyline": "Hat_SnatcherContract_DeathWish_RiftCollapse_Alps", + "Wound-Up Windmill": "Hat_SnatcherContract_DeathWish_FastWindmill", + "The Illness has Speedrun": "Hat_SnatcherContract_DeathWish_Speedrun_Illness", + "Snatcher Coins in Alpine Skyline": "Hat_SnatcherContract_DeathWish_Tokens_Alps", + "Camera Tourist": "Hat_SnatcherContract_DeathWish_CameraTourist_1", + + "The Mustache Gauntlet": "Hat_SnatcherContract_DeathWish_HardCastle", + "No More Bad Guys": "Hat_SnatcherContract_DeathWish_MuGirlEX", + + "Seal the Deal": "Hat_SnatcherContract_DeathWish_BossRushEX", + "Rift Collapse: Deep Sea": "Hat_SnatcherContract_DeathWish_RiftCollapse_Cruise", + "Cruisin' for a Bruisin'": "Hat_SnatcherContract_DeathWish_EndlessTasks", + + "Community Rift: Rhythm Jump Studio": "Hat_SnatcherContract_DeathWish_CommunityRift_RhythmJump", + "Community Rift: Twilight Travels": "Hat_SnatcherContract_DeathWish_CommunityRift_TwilightTravels", + "Community Rift: The Mountain Rift": "Hat_SnatcherContract_DeathWish_CommunityRift_MountainRift", + + "Snatcher Coins in Nyakuza Metro": "Hat_SnatcherContract_DeathWish_Tokens_Metro", +} + + +def create_dw_regions(world: World): + if world.multiworld.DWExcludeAnnoyingContracts[world.player].value > 0: + for name in annoying_dws: + world.get_excluded_dws().append(name) + + if world.multiworld.DWEnableBonus[world.player].value == 0 \ + or world.multiworld.DWAutoCompleteBonuses[world.player].value > 0: + for name in death_wishes: + world.get_excluded_bonuses().append(name) + elif world.multiworld.DWExcludeAnnoyingBonuses[world.player].value > 0: + for name in annoying_bonuses: + world.get_excluded_bonuses().append(name) + + if world.multiworld.DWExcludeCandles[world.player].value > 0: + for name in dw_candles: + if name in world.get_excluded_dws(): + continue + world.get_excluded_dws().append(name) + + spaceship = world.multiworld.get_region("Spaceship", world.player) + dw_map: Region = create_region(world, "Death Wish Map") + entrance = connect_regions(spaceship, dw_map, "-> Death Wish Map", world.player) + + add_rule(entrance, lambda state: state.has("Time Piece", world.player, + world.multiworld.DWTimePieceRequirement[world.player].value)) + + if world.multiworld.DWShuffle[world.player].value > 0: + dw_list: List[str] = [] + for name in death_wishes.keys(): + if not world.is_dlc2() and name == "Snatcher Coins in Nyakuza Metro" or world.is_dw_excluded(name): + continue + + dw_list.append(name) + + world.random.shuffle(dw_list) + count = world.random.randint(world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + dw_shuffle: List[str] = [] + total = min(len(dw_list), count) + for i in range(total): + dw_shuffle.append(dw_list[i]) + + # Seal the Deal is always last if it's the goal + if world.multiworld.EndGoal[world.player].value == 3: + if "Seal the Deal" in dw_shuffle: + dw_shuffle.remove("Seal the Deal") + + dw_shuffle.append("Seal the Deal") + + world.set_dw_shuffle(dw_shuffle) + prev_dw: Region + for i in range(len(dw_shuffle)): + name = dw_shuffle[i] + dw = create_region(world, name) + + if i == 0: + connect_regions(dw_map, dw, f"-> {name}", world.player) + else: + connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player) + + loc_id = death_wishes[name] + main_objective = HatInTimeLocation(world.player, f"{name} - Main Objective", loc_id, dw) + full_clear = HatInTimeLocation(world.player, f"{name} - All Clear", loc_id + 1, dw) + main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {name}", None, dw) + bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {name}", None, dw) + main_stamp.show_in_spoiler = False + bonus_stamps.show_in_spoiler = False + dw.locations.append(main_stamp) + dw.locations.append(bonus_stamps) + + main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {name}", + ItemClassification.progression, None, world.player)) + bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {name}", + ItemClassification.progression, None, world.player)) + + if name in world.get_excluded_dws(): + main_objective.progress_type = LocationProgressType.EXCLUDED + full_clear.progress_type = LocationProgressType.EXCLUDED + elif world.is_bonus_excluded(name): + full_clear.progress_type = LocationProgressType.EXCLUDED + + dw.locations.append(main_objective) + dw.locations.append(full_clear) + prev_dw = dw + else: + for key, loc_id in death_wishes.items(): + if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + world.get_excluded_dws().append(key) + continue + + dw = create_region(world, key) + + if key == "Beat the Heat": + connect_regions(dw_map, dw, "-> Beat the Heat", world.player) + elif key in dw_prereqs.keys(): + for name in dw_prereqs[key]: + parent = world.multiworld.get_region(name, world.player) + connect_regions(parent, dw, f"{parent.name} -> {key}", world.player) + + main_objective = HatInTimeLocation(world.player, f"{key} - Main Objective", loc_id, dw) + full_clear = HatInTimeLocation(world.player, f"{key} - All Clear", loc_id+1, dw) + main_stamp = HatInTimeLocation(world.player, f"Main Stamp - {key}", None, dw) + bonus_stamps = HatInTimeLocation(world.player, f"Bonus Stamps - {key}", None, dw) + main_stamp.show_in_spoiler = False + bonus_stamps.show_in_spoiler = False + dw.locations.append(main_stamp) + dw.locations.append(bonus_stamps) + + main_stamp.place_locked_item(HatInTimeItem(f"1 Stamp - {key}", + ItemClassification.progression, None, world.player)) + bonus_stamps.place_locked_item(HatInTimeItem(f"2 Stamps - {key}", + ItemClassification.progression, None, world.player)) + + if key in world.get_excluded_dws(): + main_objective.progress_type = LocationProgressType.EXCLUDED + full_clear.progress_type = LocationProgressType.EXCLUDED + elif world.is_bonus_excluded(key): + full_clear.progress_type = LocationProgressType.EXCLUDED + + dw.locations.append(main_objective) + dw.locations.append(full_clear) diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py new file mode 100644 index 0000000000..c448484036 --- /dev/null +++ b/worlds/ahit/DeathWishRules.py @@ -0,0 +1,539 @@ +from worlds.AutoWorld import World, CollectionState +from .Rules import can_use_hat, can_use_hookshot, can_hit, zipline_logic, get_difficulty, has_paintings +from .Types import HatType, Difficulty, HatInTimeLocation, HatInTimeItem, LocData +from .DeathWishLocations import dw_prereqs, dw_candles +from BaseClasses import Entrance, Location, ItemClassification +from worlds.generic.Rules import add_rule, set_rule +from typing import List, Callable +from .Regions import act_chapters +from .Locations import zero_jumps, zero_jumps_expert, zero_jumps_hard, death_wishes + +# Any speedruns expect the player to have Sprint Hat +dw_requirements = { + "Beat the Heat": LocData(umbrella=True), + "So You're Back From Outer Space": LocData(hookshot=True), + "She Speedran from Outer Space": LocData(required_hats=[HatType.SPRINT]), + "Mafia's Jumps": LocData(required_hats=[HatType.ICE]), + "Vault Codes in the Wind": LocData(required_hats=[HatType.SPRINT]), + + "Security Breach": LocData(hit_requirement=1), + "10 Seconds until Self-Destruct": LocData(hookshot=True), + "Community Rift: Rhythm Jump Studio": LocData(required_hats=[HatType.ICE]), + + "Speedrun Well": LocData(hookshot=True, hit_requirement=1, required_hats=[HatType.SPRINT]), + "Boss Rush": LocData(umbrella=True, hookshot=True), + "Community Rift: Twilight Travels": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + + "Bird Sanctuary": LocData(hookshot=True), + "Wound-Up Windmill": LocData(hookshot=True), + "The Illness has Speedrun": LocData(hookshot=True), + "Community Rift: The Mountain Rift": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + "Camera Tourist": LocData(misc_required=["Camera Badge"]), + + "The Mustache Gauntlet": LocData(hookshot=True, required_hats=[HatType.DWELLER]), + + "Rift Collapse - Deep Sea": LocData(hookshot=True), +} + +# Includes main objective requirements +dw_bonus_requirements = { + # Some One-Hit Hero requirements need badge pins as well because of Hookshot + "So You're Back From Outer Space": LocData(required_hats=[HatType.SPRINT]), + "Encore! Encore!": LocData(misc_required=["One-Hit Hero Badge"]), + + "10 Seconds until Self-Destruct": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + + "Boss Rush": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + "Community Rift: Twilight Travels": LocData(required_hats=[HatType.BREWING]), + + "Bird Sanctuary": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"], required_hats=[HatType.DWELLER]), + "Wound-Up Windmill": LocData(misc_required=["One-Hit Hero Badge", "Badge Pin"]), + "The Illness has Speedrun": LocData(required_hats=[HatType.SPRINT]), + + "The Mustache Gauntlet": LocData(required_hats=[HatType.ICE]), + + "Rift Collapse - Deep Sea": LocData(required_hats=[HatType.DWELLER]), +} + +dw_stamp_costs = { + "So You're Back From Outer Space": 2, + "Collect-a-thon": 5, + "She Speedran from Outer Space": 8, + "Encore! Encore!": 10, + + "Security Breach": 4, + "The Great Big Hootenanny": 7, + "10 Seconds until Self-Destruct": 15, + "Killing Two Birds": 25, + "Snatcher Coins in Nyakuza Metro": 30, + + "Speedrun Well": 10, + "Boss Rush": 15, + "Quality Time with Snatcher": 20, + "Breaching the Contract": 40, + + "Bird Sanctuary": 15, + "Wound-Up Windmill": 30, + "The Illness has Speedrun": 35, + + "The Mustache Gauntlet": 35, + "No More Bad Guys": 50, + "Seal the Deal": 70, +} + +required_snatcher_coins = { + "Snatcher Coins in Mafia Town": ["Snatcher Coin - Top of HQ", "Snatcher Coin - Top of Tower", + "Snatcher Coin - Under Ruined Tower"], + + "Snatcher Coins in Battle of the Birds": ["Snatcher Coin - Top of Red House", "Snatcher Coin - Train Rush", + "Snatcher Coin - Picture Perfect"], + + "Snatcher Coins in Subcon Forest": ["Snatcher Coin - Swamp Tree", "Snatcher Coin - Manor Roof", + "Snatcher Coin - Giant Time Piece"], + + "Snatcher Coins in Alpine Skyline": ["Snatcher Coin - Goat Village Top", "Snatcher Coin - Lava Cake", + "Snatcher Coin - Windmill"], + + "Snatcher Coins in Nyakuza Metro": ["Snatcher Coin - Green Clean Tower", "Snatcher Coin - Bluefin Cat Train", + "Snatcher Coin - Pink Paw Fence"], +} + + +def set_dw_rules(world: World): + if "Snatcher's Hit List" not in world.get_excluded_dws() \ + or "Camera Tourist" not in world.get_excluded_dws(): + set_enemy_rules(world) + + dw_list: List[str] = [] + if world.multiworld.DWShuffle[world.player].value > 0: + dw_list = world.get_dw_shuffle() + else: + for name in death_wishes.keys(): + dw_list.append(name) + + for name in dw_list: + if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + dw = world.multiworld.get_region(name, world.player) + temp_list: List[Location] = [] + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + main_stamp = world.multiworld.get_location(f"Main Stamp - {name}", world.player) + bonus_stamps = world.multiworld.get_location(f"Bonus Stamps - {name}", world.player) + temp_list.append(main_objective) + temp_list.append(full_clear) + + if world.multiworld.DWShuffle[world.player].value == 0: + if name in dw_stamp_costs.keys(): + for entrance in dw.entrances: + add_rule(entrance, lambda state, n=name: get_total_dw_stamps(state, world) >= dw_stamp_costs[n]) + + if world.multiworld.DWEnableBonus[world.player].value == 0: + # place nothing, but let the locations exist still, so we can use them for bonus stamp rules + full_clear.address = None + full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, world.player)) + full_clear.show_in_spoiler = False + + # No need for rules if excluded - stamps will be auto-granted + if world.is_dw_excluded(name): + continue + + # Specific Rules + modify_dw_rules(world, name) + + main_rule: Callable[[CollectionState], bool] + for i in range(len(temp_list)): + loc = temp_list[i] + data: LocData + + if loc.name == main_objective.name: + data = dw_requirements.get(name) + else: + data = dw_bonus_requirements.get(name) + + if data is None: + continue + + if data.hookshot: + add_rule(loc, lambda state: can_use_hookshot(state, world)) + + for hat in data.required_hats: + if hat is not HatType.NONE: + add_rule(loc, lambda state, h=hat: can_use_hat(state, world, h)) + + for misc in data.misc_required: + add_rule(loc, lambda state, item=misc: state.has(item, world.player)) + + if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(loc, lambda state: state.has("Umbrella", world.player)) + + if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(loc, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) + + if data.hit_requirement > 0: + if data.hit_requirement == 1: + add_rule(loc, lambda state: can_hit(state, world)) + elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) + add_rule(loc, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + + main_rule = main_objective.access_rule + + if loc.name == main_objective.name: + add_rule(main_stamp, loc.access_rule) + elif loc.name == full_clear.name: + add_rule(loc, main_rule) + # Only set bonus stamp rules if we don't auto complete bonuses + if world.multiworld.DWAutoCompleteBonuses[world.player].value == 0 \ + and not world.is_bonus_excluded(loc.name): + add_rule(bonus_stamps, loc.access_rule) + + if world.multiworld.DWShuffle[world.player].value > 0: + dw_shuffle = world.get_dw_shuffle() + for i in range(len(dw_shuffle)): + if i == 0: + continue + + name = dw_shuffle[i] + prev_dw = world.multiworld.get_region(dw_shuffle[i-1], world.player) + entrance = world.multiworld.get_entrance(f"{prev_dw.name} -> {name}", world.player) + add_rule(entrance, lambda state, n=prev_dw.name: state.has(f"1 Stamp - {n}", world.player)) + else: + for key, reqs in dw_prereqs.items(): + if key == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + access_rules: List[Callable[[CollectionState], bool]] = [] + entrances: List[Entrance] = [] + + for parent in reqs: + entrance = world.multiworld.get_entrance(f"{parent} -> {key}", world.player) + entrances.append(entrance) + + if not world.is_dw_excluded(parent): + access_rules.append(lambda state, n=parent: state.has(f"1 Stamp - {n}", world.player)) + + for entrance in entrances: + for rule in access_rules: + add_rule(entrance, rule) + + if world.multiworld.EndGoal[world.player].value == 3: + world.multiworld.completion_condition[world.player] = lambda state: state.has("1 Stamp - Seal the Deal", + world.player) + + +def modify_dw_rules(world: World, name: str): + difficulty: Difficulty = get_difficulty(world) + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + + if name == "The Illness has Speedrun": + # All stamps with hookshot only in Expert + if difficulty >= Difficulty.EXPERT: + set_rule(full_clear, lambda state: True) + else: + add_rule(main_objective, lambda state: state.has("Umbrella", world.player)) + + elif name == "The Mustache Gauntlet": + add_rule(main_objective, lambda state: state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.ICE) or can_use_hat(state, world, HatType.BREWING)) + + elif name == "Vault Codes in the Wind": + # Sprint is normally expected here + if difficulty >= Difficulty.HARD: + set_rule(main_objective, lambda state: True) + + elif name == "Speedrun Well": + # All stamps with nothing :) + if difficulty >= Difficulty.EXPERT: + set_rule(main_objective, lambda state: True) + + elif name == "Mafia's Jumps": + if difficulty >= Difficulty.HARD: + set_rule(main_objective, lambda state: True) + set_rule(full_clear, lambda state: True) + + elif name == "So You're Back from Outer Space": + # Without Hookshot + if difficulty >= Difficulty.HARD: + set_rule(main_objective, lambda state: True) + + elif name == "Wound-Up Windmill": + # No badge pin required. Player can switch to One Hit Hero after the checkpoint and do level without it. + if difficulty >= Difficulty.MODERATE: + set_rule(full_clear, lambda state: can_use_hookshot(state, world) + and state.has("One-Hit Hero Badge", world.player)) + + if name in dw_candles: + set_candle_dw_rules(name, world) + + +def get_total_dw_stamps(state: CollectionState, world: World) -> int: + if world.multiworld.DWShuffle[world.player].value > 0: + return 999 # no stamp costs in death wish shuffle + + count: int = 0 + + for name in death_wishes: + if name == "Snatcher Coins in Nyakuza Metro" and not world.is_dlc2(): + continue + + if state.has(f"1 Stamp - {name}", world.player): + count += 1 + else: + continue + + if state.has(f"2 Stamps - {name}", world.player): + count += 2 + elif name not in dw_candles: + # most non-candle bonus requirements allow the player to get the other stamp (like not having One Hit Hero) + count += 1 + + return count + + +def set_candle_dw_rules(name: str, world: World): + main_objective = world.multiworld.get_location(f"{name} - Main Objective", world.player) + full_clear = world.multiworld.get_location(f"{name} - All Clear", world.player) + + if name == "Zero Jumps": + add_rule(main_objective, lambda state: get_zero_jump_clear_count(state, world) >= 1) + add_rule(full_clear, lambda state: get_zero_jump_clear_count(state, world) >= 4 + and state.has("Train Rush (Zero Jumps)", world.player) and can_use_hat(state, world, HatType.ICE)) + + # No Ice Hat/painting required in Expert for Toilet Zero Jumps + # This painting wall can only be skipped via cherry hover. + if get_difficulty(world) < Difficulty.EXPERT or world.multiworld.NoPaintingSkips[world.player].value == 1: + set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world) + and has_paintings(state, world, 1, False)) + else: + set_rule(world.multiworld.get_location("Toilet of Doom (Zero Jumps)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world)) + + set_rule(world.multiworld.get_location("Contractual Obligations (Zero Jumps)", world.player), + lambda state: has_paintings(state, world, 1, False)) + + elif name == "Snatcher's Hit List": + add_rule(main_objective, lambda state: state.has("Mafia Goon", world.player)) + add_rule(full_clear, lambda state: get_reachable_enemy_count(state, world) >= 12) + + elif name == "Camera Tourist": + add_rule(main_objective, lambda state: get_reachable_enemy_count(state, world) >= 8) + add_rule(full_clear, lambda state: can_reach_all_bosses(state, world) + and state.has("Triple Enemy Photo", world.player)) + + elif "Snatcher Coins" in name: + for coin in required_snatcher_coins[name]: + add_rule(main_objective, lambda state: state.has(coin, world.player), "or") + add_rule(full_clear, lambda state: state.has(coin, world.player)) + + +def get_zero_jump_clear_count(state: CollectionState, world: World) -> int: + total: int = 0 + + for name in act_chapters.keys(): + n = f"{name} (Zero Jumps)" + if n not in zero_jumps: + continue + + if get_difficulty(world) < Difficulty.HARD and n in zero_jumps_hard: + continue + + if get_difficulty(world) < Difficulty.EXPERT and n in zero_jumps_expert: + continue + + if not state.has(n, world.player): + continue + + total += 1 + + return total + + +def get_reachable_enemy_count(state: CollectionState, world: World) -> int: + count: int = 0 + for enemy in hit_list.keys(): + if enemy in bosses: + continue + + if state.has(enemy, world.player): + count += 1 + + return count + + +def can_reach_all_bosses(state: CollectionState, world: World) -> bool: + for boss in bosses: + if not state.has(boss, world.player): + return False + + return True + + +def create_enemy_events(world: World): + no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() + + for enemy, regions in hit_list.items(): + if no_tourist and enemy in bosses: + continue + + for area in regions: + if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1(): + continue + + if area == "Time Rift - Tour" and (not world.is_dlc1() + or world.multiworld.ExcludeTour[world.player].value > 0): + continue + + if area == "Bluefin Tunnel" and not world.is_dlc2(): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes.keys() \ + and area not in world.get_dw_shuffle(): + continue + + region = world.multiworld.get_region(area, world.player) + event = HatInTimeLocation(world.player, f"{enemy} - {area}", None, region) + event.place_locked_item(HatInTimeItem(enemy, ItemClassification.progression, None, world.player)) + region.locations.append(event) + event.show_in_spoiler = False + + for name in triple_enemy_locations: + if name == "Time Rift - Tour" and (not world.is_dlc1() or world.multiworld.ExcludeTour[world.player].value > 0): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and name in death_wishes.keys() \ + and name not in world.get_dw_shuffle(): + continue + + region = world.multiworld.get_region(name, world.player) + event = HatInTimeLocation(world.player, f"Triple Enemy Photo - {name}", None, region) + event.place_locked_item(HatInTimeItem("Triple Enemy Photo", ItemClassification.progression, None, world.player)) + region.locations.append(event) + event.show_in_spoiler = False + if name == "The Mustache Gauntlet": + add_rule(event, lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) + + +def set_enemy_rules(world: World): + no_tourist = "Camera Tourist" in world.get_excluded_dws() or "Camera Tourist" in world.get_excluded_bonuses() + + for enemy, regions in hit_list.items(): + if no_tourist and enemy in bosses: + continue + + for area in regions: + if (area == "Bon Voyage!" or area == "Time Rift - Deep Sea") and not world.is_dlc1(): + continue + + if area == "Time Rift - Tour" and (not world.is_dlc1() + or world.multiworld.ExcludeTour[world.player].value > 0): + continue + + if area == "Bluefin Tunnel" and not world.is_dlc2(): + continue + + if world.multiworld.DWShuffle[world.player].value > 0 and area in death_wishes \ + and area not in world.get_dw_shuffle(): + continue + + event = world.multiworld.get_location(f"{enemy} - {area}", world.player) + + if enemy == "Toxic Flower": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + if area == "The Illness has Spread": + add_rule(event, lambda state: not zipline_logic(world) or + state.has("Zipline Unlock - The Birdhouse Path", world.player) + or state.has("Zipline Unlock - The Lava Cake Path", world.player) + or state.has("Zipline Unlock - The Windmill Path", world.player)) + + elif enemy == "Director": + if area == "Dead Bird Studio Basement": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + elif enemy == "Snatcher" or enemy == "Mustache Girl": + if area == "Boss Rush": + # need to be able to kill toilet and snatcher + add_rule(event, lambda state: can_hit(state, world) and can_use_hookshot(state, world)) + if enemy == "Mustache Girl": + add_rule(event, lambda state: can_hit(state, world, True) and can_use_hookshot(state, world)) + + elif area == "The Finale" and enemy == "Mustache Girl": + add_rule(event, lambda state: can_use_hookshot(state, world) + and can_use_hat(state, world, HatType.DWELLER)) + + elif enemy == "Shock Squid" or enemy == "Ninja Cat": + if area == "Time Rift - Deep Sea": + add_rule(event, lambda state: can_use_hookshot(state, world)) + + +# Enemies for Snatcher's Hit List/Camera Tourist, and where to find them +hit_list = { + "Mafia Goon": ["Mafia Town Area", "Time Rift - Mafia of Cooks", "Time Rift - Tour", + "Bon Voyage!", "The Mustache Gauntlet", "Rift Collapse: Mafia of Cooks", + "So You're Back From Outer Space"], + + "Sleepy Raccoon": ["She Came from Outer Space", "Down with the Mafia!", "The Twilight Bell", + "She Speedran from Outer Space", "Mafia's Jumps", "The Mustache Gauntlet", + "Time Rift - Sleepy Subcon", "Rift Collapse: Sleepy Subcon"], + + "UFO": ["Picture Perfect", "So You're Back From Outer Space", "Community Rift: Rhythm Jump Studio"], + + "Rat": ["Down with the Mafia!", "Bluefin Tunnel"], + + "Shock Squid": ["Bon Voyage!", "Time Rift - Sleepy Subcon", "Time Rift - Deep Sea", + "Rift Collapse: Sleepy Subcon"], + + "Shromb Egg": ["The Birdhouse", "Bird Sanctuary"], + + "Spider": ["Subcon Forest Area", "The Mustache Gauntlet", "Speedrun Well", + "The Lava Cake", "The Windmill"], + + "Crow": ["Mafia Town Area", "The Birdhouse", "Time Rift - Tour", "Bird Sanctuary", + "Time Rift - Alpine Skyline", "Rift Collapse: Alpine Skyline"], + + "Pompous Crow": ["The Birdhouse", "Time Rift - The Lab", "Bird Sanctuary", "The Mustache Gauntlet"], + + "Fiery Crow": ["The Finale", "The Lava Cake", "The Mustache Gauntlet"], + + "Express Owl": ["The Finale", "Time Rift - The Owl Express", "Time Rift - Deep Sea"], + + "Ninja Cat": ["The Birdhouse", "The Windmill", "Bluefin Tunnel", "The Mustache Gauntlet", + "Time Rift - Curly Tail Trail", "Time Rift - Alpine Skyline", "Time Rift - Deep Sea", + "Rift Collapse: Alpine Skyline"], + + # Bosses + "Mafia Boss": ["Down with the Mafia!", "Encore! Encore!", "Boss Rush"], + + "Conductor": ["Dead Bird Studio Basement", "Killing Two Birds", "Boss Rush"], + "Toilet": ["Toilet of Doom", "Boss Rush"], + + "Snatcher": ["Your Contract has Expired", "Breaching the Contract", "Boss Rush", + "Quality Time with Snatcher"], + + "Toxic Flower": ["The Illness has Spread", "The Illness has Speedrun"], + + "Mustache Girl": ["The Finale", "Boss Rush", "No More Bad Guys"], +} + +# Camera Tourist has a bonus that requires getting three different types of enemies in one picture. +triple_enemy_locations = [ + "She Came from Outer Space", + "She Speedran from Outer Space", + "Mafia's Jumps", + "The Mustache Gauntlet", + "The Birdhouse", + "Bird Sanctuary", + "Time Rift - Tour", +] + +bosses = [ + "Mafia Boss", + "Conductor", + "Toilet", + "Snatcher", + "Toxic Flower", + "Mustache Girl", +] diff --git a/worlds/ahit/Items.py b/worlds/ahit/Items.py new file mode 100644 index 0000000000..869f998a9d --- /dev/null +++ b/worlds/ahit/Items.py @@ -0,0 +1,286 @@ +from BaseClasses import Item, ItemClassification +from worlds.AutoWorld import World +from .Types import HatDLC, HatType, hat_type_to_item, Difficulty, ItemData, HatInTimeItem +from .Locations import get_total_locations +from .Rules import get_difficulty +from typing import Optional, List, Dict + + +def create_itempool(world: World) -> List[Item]: + itempool: List[Item] = [] + if not world.is_dw_only() and world.multiworld.HatItems[world.player].value == 0: + calculate_yarn_costs(world) + yarn_pool: List[Item] = create_multiple_items(world, "Yarn", + world.multiworld.YarnAvailable[world.player].value, + ItemClassification.progression_skip_balancing) + + for i in range(int(len(yarn_pool) * (0.01 * world.multiworld.YarnBalancePercent[world.player].value))): + yarn_pool[i].classification = ItemClassification.progression + + itempool += yarn_pool + + for name in item_table.keys(): + if name == "Yarn": + continue + + if not item_dlc_enabled(world, name): + continue + + if world.multiworld.HatItems[world.player].value == 0 and name in hat_type_to_item.values(): + continue + + item_type: ItemClassification = item_table.get(name).classification + + if world.is_dw_only(): + if item_type is ItemClassification.progression \ + or item_type is ItemClassification.progression_skip_balancing: + continue + else: + if name == "Scooter Badge": + if world.multiworld.CTRLogic[world.player].value >= 1 or get_difficulty(world) >= Difficulty.MODERATE: + item_type = ItemClassification.progression + elif name == "No Bonk Badge": + if get_difficulty(world) >= Difficulty.MODERATE: + item_type = ItemClassification.progression + + # some death wish bonuses require one hit hero + hookshot + if world.is_dw() and name == "Badge Pin" and not world.is_dw_only(): + item_type = ItemClassification.progression + + if item_type is ItemClassification.filler or item_type is ItemClassification.trap: + continue + + if name in act_contracts.keys() and world.multiworld.ShuffleActContracts[world.player].value == 0: + continue + + if name in alps_hooks.keys() and world.multiworld.ShuffleAlpineZiplines[world.player].value == 0: + continue + + if name == "Progressive Painting Unlock" \ + and world.multiworld.ShuffleSubconPaintings[world.player].value == 0: + continue + + if world.multiworld.StartWithCompassBadge[world.player].value > 0 and name == "Compass Badge": + continue + + if name == "Time Piece": + tp_count: int = 40 + max_extra: int = 0 + if world.is_dlc1(): + max_extra += 6 + + if world.is_dlc2(): + max_extra += 10 + + tp_count += min(max_extra, world.multiworld.MaxExtraTimePieces[world.player].value) + tp_list: List[Item] = create_multiple_items(world, name, tp_count, item_type) + + for i in range(int(len(tp_list) * (0.01 * world.multiworld.TimePieceBalancePercent[world.player].value))): + tp_list[i].classification = ItemClassification.progression + + itempool += tp_list + continue + + itempool += create_multiple_items(world, name, item_frequencies.get(name, 1), item_type) + + itempool += create_junk_items(world, get_total_locations(world) - len(itempool)) + return itempool + + +def calculate_yarn_costs(world: World): + mw = world.multiworld + p = world.player + min_yarn_cost = int(min(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + max_yarn_cost = int(max(mw.YarnCostMin[p].value, mw.YarnCostMax[p].value)) + + max_cost: int = 0 + for i in range(5): + cost: int = mw.random.randint(min(min_yarn_cost, max_yarn_cost), max(max_yarn_cost, min_yarn_cost)) + world.get_hat_yarn_costs()[HatType(i)] = cost + max_cost += cost + + available_yarn: int = mw.YarnAvailable[p].value + if max_cost > available_yarn: + mw.YarnAvailable[p].value = max_cost + available_yarn = max_cost + + if max_cost + mw.MinExtraYarn[p].value > available_yarn: + mw.YarnAvailable[p].value += (max_cost + mw.MinExtraYarn[p].value) - available_yarn + + +def item_dlc_enabled(world: World, name: str) -> bool: + data = item_table[name] + + if data.dlc_flags == HatDLC.none: + return True + elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1(): + return True + elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2(): + return True + elif data.dlc_flags == HatDLC.death_wish and world.is_dw(): + return True + + return False + + +def create_item(world: World, name: str) -> Item: + data = item_table[name] + return HatInTimeItem(name, data.classification, data.code, world.player) + + +def create_multiple_items(world: World, name: str, count: int = 1, + item_type: Optional[ItemClassification] = ItemClassification.progression) -> List[Item]: + + data = item_table[name] + itemlist: List[Item] = [] + + for i in range(count): + itemlist += [HatInTimeItem(name, item_type, data.code, world.player)] + + return itemlist + + +def create_junk_items(world: World, count: int) -> List[Item]: + trap_chance = world.multiworld.TrapChance[world.player].value + junk_pool: List[Item] = [] + junk_list: Dict[str, int] = {} + trap_list: Dict[str, int] = {} + ic: ItemClassification + + for name in item_table.keys(): + ic = item_table[name].classification + if ic == ItemClassification.filler: + if world.is_dw_only() and "Pons" in name: + continue + + junk_list[name] = junk_weights.get(name) + + elif trap_chance > 0 and ic == ItemClassification.trap: + if name == "Baby Trap": + trap_list[name] = world.multiworld.BabyTrapWeight[world.player].value + elif name == "Laser Trap": + trap_list[name] = world.multiworld.LaserTrapWeight[world.player].value + elif name == "Parade Trap": + trap_list[name] = world.multiworld.ParadeTrapWeight[world.player].value + + for i in range(count): + if trap_chance > 0 and world.random.randint(1, 100) <= trap_chance: + junk_pool += [world.create_item( + world.random.choices(list(trap_list.keys()), weights=list(trap_list.values()), k=1)[0])] + else: + junk_pool += [world.create_item( + world.random.choices(list(junk_list.keys()), weights=list(junk_list.values()), k=1)[0])] + + return junk_pool + + +ahit_items = { + "Yarn": ItemData(2000300001, ItemClassification.progression_skip_balancing), + "Time Piece": ItemData(2000300002, ItemClassification.progression_skip_balancing), + + # for HatItems option + "Sprint Hat": ItemData(2000300049, ItemClassification.progression), + "Brewing Hat": ItemData(2000300050, ItemClassification.progression), + "Ice Hat": ItemData(2000300051, ItemClassification.progression), + "Dweller Mask": ItemData(2000300052, ItemClassification.progression), + "Time Stop Hat": ItemData(2000300053, ItemClassification.progression), + + # Relics + "Relic (Burger Patty)": ItemData(2000300006, ItemClassification.progression), + "Relic (Burger Cushion)": ItemData(2000300007, ItemClassification.progression), + "Relic (Mountain Set)": ItemData(2000300008, ItemClassification.progression), + "Relic (Train)": ItemData(2000300009, ItemClassification.progression), + "Relic (UFO)": ItemData(2000300010, ItemClassification.progression), + "Relic (Cow)": ItemData(2000300011, ItemClassification.progression), + "Relic (Cool Cow)": ItemData(2000300012, ItemClassification.progression), + "Relic (Tin-foil Hat Cow)": ItemData(2000300013, ItemClassification.progression), + "Relic (Crayon Box)": ItemData(2000300014, ItemClassification.progression), + "Relic (Red Crayon)": ItemData(2000300015, ItemClassification.progression), + "Relic (Blue Crayon)": ItemData(2000300016, ItemClassification.progression), + "Relic (Green Crayon)": ItemData(2000300017, ItemClassification.progression), + + # Badges + "Projectile Badge": ItemData(2000300024, ItemClassification.useful), + "Fast Hatter Badge": ItemData(2000300025, ItemClassification.useful), + "Hover Badge": ItemData(2000300026, ItemClassification.useful), + "Hookshot Badge": ItemData(2000300027, ItemClassification.progression), + "Item Magnet Badge": ItemData(2000300028, ItemClassification.useful), + "No Bonk Badge": ItemData(2000300029, ItemClassification.useful), + "Compass Badge": ItemData(2000300030, ItemClassification.useful), + "Scooter Badge": ItemData(2000300031, ItemClassification.useful), + "One-Hit Hero Badge": ItemData(2000300038, ItemClassification.progression, HatDLC.death_wish), + "Camera Badge": ItemData(2000300042, ItemClassification.progression, HatDLC.death_wish), + + # Other + "Badge Pin": ItemData(2000300043, ItemClassification.useful), + "Umbrella": ItemData(2000300033, ItemClassification.progression), + "Progressive Painting Unlock": ItemData(2000300003, ItemClassification.progression), + + # Garbage items + "25 Pons": ItemData(2000300034, ItemClassification.filler), + "50 Pons": ItemData(2000300035, ItemClassification.filler), + "100 Pons": ItemData(2000300036, ItemClassification.filler), + "Health Pon": ItemData(2000300037, ItemClassification.filler), + "Random Cosmetic": ItemData(2000300044, ItemClassification.filler), + + # Traps + "Baby Trap": ItemData(2000300039, ItemClassification.trap), + "Laser Trap": ItemData(2000300040, ItemClassification.trap), + "Parade Trap": ItemData(2000300041, ItemClassification.trap), + + # DLC1 items + "Relic (Cake Stand)": ItemData(2000300018, ItemClassification.progression, HatDLC.dlc1), + "Relic (Shortcake)": ItemData(2000300019, ItemClassification.progression, HatDLC.dlc1), + "Relic (Chocolate Cake Slice)": ItemData(2000300020, ItemClassification.progression, HatDLC.dlc1), + "Relic (Chocolate Cake)": ItemData(2000300021, ItemClassification.progression, HatDLC.dlc1), + + # DLC2 items + "Relic (Necklace Bust)": ItemData(2000300022, ItemClassification.progression, HatDLC.dlc2), + "Relic (Necklace)": ItemData(2000300023, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Yellow": ItemData(2000300045, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Green": ItemData(2000300046, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Blue": ItemData(2000300047, ItemClassification.progression, HatDLC.dlc2), + "Metro Ticket - Pink": ItemData(2000300048, ItemClassification.progression, HatDLC.dlc2), +} + +act_contracts = { + "Snatcher's Contract - The Subcon Well": ItemData(2000300200, ItemClassification.progression), + "Snatcher's Contract - Toilet of Doom": ItemData(2000300201, ItemClassification.progression), + "Snatcher's Contract - Queen Vanessa's Manor": ItemData(2000300202, ItemClassification.progression), + "Snatcher's Contract - Mail Delivery Service": ItemData(2000300203, ItemClassification.progression), +} + +alps_hooks = { + "Zipline Unlock - The Birdhouse Path": ItemData(2000300204, ItemClassification.progression), + "Zipline Unlock - The Lava Cake Path": ItemData(2000300205, ItemClassification.progression), + "Zipline Unlock - The Windmill Path": ItemData(2000300206, ItemClassification.progression), + "Zipline Unlock - The Twilight Bell Path": ItemData(2000300207, ItemClassification.progression), +} + +relic_groups = { + "Burger": {"Relic (Burger Patty)", "Relic (Burger Cushion)"}, + "Train": {"Relic (Mountain Set)", "Relic (Train)"}, + "UFO": {"Relic (UFO)", "Relic (Cow)", "Relic (Cool Cow)", "Relic (Tin-foil Hat Cow)"}, + "Crayon": {"Relic (Crayon Box)", "Relic (Red Crayon)", "Relic (Blue Crayon)", "Relic (Green Crayon)"}, + "Cake": {"Relic (Cake Stand)", "Relic (Chocolate Cake)", "Relic (Chocolate Cake Slice)", "Relic (Shortcake)"}, + "Necklace": {"Relic (Necklace Bust)", "Relic (Necklace)"}, +} + +item_frequencies = { + "Badge Pin": 2, + "Progressive Painting Unlock": 3, +} + +junk_weights = { + "25 Pons": 50, + "50 Pons": 25, + "100 Pons": 10, + "Health Pon": 35, + "Random Cosmetic": 35, +} + +item_table = { + **ahit_items, + **act_contracts, + **alps_hooks, +} diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py new file mode 100644 index 0000000000..bf31c8cba8 --- /dev/null +++ b/worlds/ahit/Locations.py @@ -0,0 +1,977 @@ +from worlds.AutoWorld import World +from .Types import HatDLC, HatType, LocData, Difficulty +from typing import Dict +from .Options import TasksanityCheckCount + + +TASKSANITY_START_ID = 2000300204 + + +def get_total_locations(world: World) -> int: + total: int = 0 + + if not world.is_dw_only(): + for (name) in location_table.keys(): + if is_location_valid(world, name): + total += 1 + + if world.is_dlc1() and world.multiworld.Tasksanity[world.player].value > 0: + total += world.multiworld.TasksanityCheckCount[world.player].value + + if world.is_dw(): + if world.multiworld.DWShuffle[world.player].value > 0: + total += len(world.get_dw_shuffle()) + if world.multiworld.DWEnableBonus[world.player].value > 0: + total += len(world.get_dw_shuffle()) + else: + total += 37 + if world.is_dlc2(): + total += 1 + + if world.multiworld.DWEnableBonus[world.player].value > 0: + total += 37 + if world.is_dlc2(): + total += 1 + + return total + + +def location_dlc_enabled(world: World, location: str) -> bool: + data = location_table.get(location) or event_locs.get(location) + + if data.dlc_flags == HatDLC.none: + return True + elif data.dlc_flags == HatDLC.dlc1 and world.is_dlc1(): + return True + elif data.dlc_flags == HatDLC.dlc2 and world.is_dlc2(): + return True + elif data.dlc_flags == HatDLC.death_wish and world.is_dw(): + return True + elif data.dlc_flags == HatDLC.dlc1_dw and world.is_dlc1() and world.is_dw(): + return True + elif data.dlc_flags == HatDLC.dlc2_dw and world.is_dlc2() and world.is_dw(): + return True + + return False + + +def is_location_valid(world: World, location: str) -> bool: + if not location_dlc_enabled(world, location): + return False + + if world.multiworld.ShuffleStorybookPages[world.player].value == 0 \ + and location in storybook_pages.keys(): + return False + + if world.multiworld.ShuffleActContracts[world.player].value == 0 \ + and location in contract_locations.keys(): + return False + + if location not in world.shop_locs and location in shop_locations: + return False + + data = location_table.get(location) or event_locs.get(location) + if world.multiworld.ExcludeTour[world.player].value > 0 and data.region == "Time Rift - Tour": + return False + + # No need for all those event items if we're not doing candles + if data.dlc_flags & HatDLC.death_wish: + if world.multiworld.DWExcludeCandles[world.player].value > 0 and location in event_locs.keys(): + return False + + if world.multiworld.DWShuffle[world.player].value > 0 \ + and data.region in death_wishes and data.region not in world.get_dw_shuffle(): + return False + + if location in zero_jumps: + if world.multiworld.DWShuffle[world.player].value > 0 and "Zero Jumps" not in world.get_dw_shuffle(): + return False + + difficulty: int = world.multiworld.LogicDifficulty[world.player].value + if location in zero_jumps_hard and difficulty < int(Difficulty.HARD): + return False + + if location in zero_jumps_expert and difficulty < int(Difficulty.EXPERT): + return False + + return True + + +def get_location_names() -> Dict[str, int]: + names = {name: data.id for name, data in location_table.items()} + id_start: int = TASKSANITY_START_ID + for i in range(TasksanityCheckCount.range_end): + names.setdefault(f"Tasksanity Check {i+1}", id_start+i) + + for (key, loc_id) in death_wishes.items(): + names.setdefault(f"{key} - Main Objective", loc_id) + names.setdefault(f"{key} - All Clear", loc_id+1) + + return names + + +ahit_locations = { + "Spaceship - Rumbi Abuse": LocData(2000301000, "Spaceship", hit_requirement=1), + + # 300000 range - Mafia Town/Batle of the Birds + "Welcome to Mafia Town - Umbrella": LocData(2000301002, "Welcome to Mafia Town"), + "Mafia Town - Old Man (Seaside Spaghetti)": LocData(2000303833, "Mafia Town Area"), + "Mafia Town - Old Man (Steel Beams)": LocData(2000303832, "Mafia Town Area"), + "Mafia Town - Blue Vault": LocData(2000302850, "Mafia Town Area"), + "Mafia Town - Green Vault": LocData(2000302851, "Mafia Town Area"), + "Mafia Town - Red Vault": LocData(2000302848, "Mafia Town Area"), + "Mafia Town - Blue Vault Brewing Crate": LocData(2000305572, "Mafia Town Area", required_hats=[HatType.BREWING]), + "Mafia Town - Plaza Under Boxes": LocData(2000304458, "Mafia Town Area"), + "Mafia Town - Small Boat": LocData(2000304460, "Mafia Town Area"), + "Mafia Town - Staircase Pon Cluster": LocData(2000304611, "Mafia Town Area"), + "Mafia Town - Palm Tree": LocData(2000304609, "Mafia Town Area"), + "Mafia Town - Port": LocData(2000305219, "Mafia Town Area"), + "Mafia Town - Docks Chest": LocData(2000303534, "Mafia Town Area"), + "Mafia Town - Ice Hat Cage": LocData(2000304831, "Mafia Town Area", required_hats=[HatType.ICE]), + "Mafia Town - Hidden Buttons Chest": LocData(2000303483, "Mafia Town Area"), + + # These can be accessed from HUMT, the above locations can't be + "Mafia Town - Dweller Boxes": LocData(2000304462, "Mafia Town Area (HUMT)"), + "Mafia Town - Ledge Chest": LocData(2000303530, "Mafia Town Area (HUMT)"), + "Mafia Town - Yellow Sphere Building Chest": LocData(2000303535, "Mafia Town Area (HUMT)"), + "Mafia Town - Beneath Scaffolding": LocData(2000304456, "Mafia Town Area (HUMT)"), + "Mafia Town - On Scaffolding": LocData(2000304457, "Mafia Town Area (HUMT)"), + "Mafia Town - Cargo Ship": LocData(2000304459, "Mafia Town Area (HUMT)"), + "Mafia Town - Beach Alcove": LocData(2000304463, "Mafia Town Area (HUMT)"), + "Mafia Town - Wood Cage": LocData(2000304606, "Mafia Town Area (HUMT)"), + "Mafia Town - Beach Patio": LocData(2000304610, "Mafia Town Area (HUMT)"), + "Mafia Town - Steel Beam Nest": LocData(2000304608, "Mafia Town Area (HUMT)"), + "Mafia Town - Top of Ruined Tower": LocData(2000304607, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]), + "Mafia Town - Hot Air Balloon": LocData(2000304829, "Mafia Town Area (HUMT)", required_hats=[HatType.ICE]), + "Mafia Town - Camera Badge 1": LocData(2000302003, "Mafia Town Area (HUMT)"), + "Mafia Town - Camera Badge 2": LocData(2000302004, "Mafia Town Area (HUMT)"), + "Mafia Town - Chest Beneath Aqueduct": LocData(2000303489, "Mafia Town Area (HUMT)"), + "Mafia Town - Secret Cave": LocData(2000305220, "Mafia Town Area (HUMT)", required_hats=[HatType.BREWING]), + "Mafia Town - Crow Chest": LocData(2000303532, "Mafia Town Area (HUMT)"), + "Mafia Town - Above Boats": LocData(2000305218, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Slip Slide Chest": LocData(2000303529, "Mafia Town Area (HUMT)"), + "Mafia Town - Behind Faucet": LocData(2000304214, "Mafia Town Area (HUMT)"), + "Mafia Town - Clock Tower Chest": LocData(2000303481, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Top of Lighthouse": LocData(2000304213, "Mafia Town Area (HUMT)", hookshot=True), + "Mafia Town - Mafia Geek Platform": LocData(2000304212, "Mafia Town Area (HUMT)"), + "Mafia Town - Behind HQ Chest": LocData(2000303486, "Mafia Town Area (HUMT)"), + + "Mafia HQ - Hallway Brewing Crate": LocData(2000305387, "Down with the Mafia!", required_hats=[HatType.BREWING]), + "Mafia HQ - Freezer Chest": LocData(2000303241, "Down with the Mafia!"), + "Mafia HQ - Secret Room": LocData(2000304979, "Down with the Mafia!", required_hats=[HatType.ICE]), + "Mafia HQ - Bathroom Stall Chest": LocData(2000303243, "Down with the Mafia!"), + + "Dead Bird Studio - Up the Ladder": LocData(2000304874, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Red Building Top": LocData(2000305024, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Behind Water Tower": LocData(2000305248, "Dead Bird Studio - Elevator Area"), + "Dead Bird Studio - Side of House": LocData(2000305247, "Dead Bird Studio - Elevator Area"), + + "Dead Bird Studio - DJ Grooves Sign Chest": LocData(2000303901, "Dead Bird Studio - Post Elevator Area", + hit_requirement=1), + + "Dead Bird Studio - Tightrope Chest": LocData(2000303898, "Dead Bird Studio - Post Elevator Area", + hit_requirement=1), + + "Dead Bird Studio - Tepee Chest": LocData(2000303899, "Dead Bird Studio - Post Elevator Area", hit_requirement=1), + + "Dead Bird Studio - Conductor Chest": LocData(2000303900, "Dead Bird Studio - Post Elevator Area", + hit_requirement=1), + + "Murder on the Owl Express - Cafeteria": LocData(2000305313, "Murder on the Owl Express"), + "Murder on the Owl Express - Luggage Room Top": LocData(2000305090, "Murder on the Owl Express"), + "Murder on the Owl Express - Luggage Room Bottom": LocData(2000305091, "Murder on the Owl Express"), + + "Murder on the Owl Express - Raven Suite Room": LocData(2000305701, "Murder on the Owl Express", + required_hats=[HatType.BREWING]), + + "Murder on the Owl Express - Raven Suite Top": LocData(2000305312, "Murder on the Owl Express"), + "Murder on the Owl Express - Lounge Chest": LocData(2000303963, "Murder on the Owl Express"), + + "Picture Perfect - Behind Badge Seller": LocData(2000304307, "Picture Perfect"), + "Picture Perfect - Hats Buy Building": LocData(2000304530, "Picture Perfect"), + + "Dead Bird Studio Basement - Window Platform": LocData(2000305432, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Cardboard Conductor": LocData(2000305059, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Above Conductor Sign": LocData(2000305057, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Logo Wall": LocData(2000305207, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Disco Room": LocData(2000305061, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Small Room": LocData(2000304813, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Vent Pipe": LocData(2000305430, "Dead Bird Studio Basement"), + "Dead Bird Studio Basement - Tightrope": LocData(2000305058, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Cameras": LocData(2000305431, "Dead Bird Studio Basement", hookshot=True), + "Dead Bird Studio Basement - Locked Room": LocData(2000305819, "Dead Bird Studio Basement", hookshot=True), + + # Subcon Forest + "Contractual Obligations - Cherry Bomb Bone Cage": LocData(2000324761, "Contractual Obligations"), + "Subcon Village - Tree Top Ice Cube": LocData(2000325078, "Subcon Forest Area"), + "Subcon Village - Graveyard Ice Cube": LocData(2000325077, "Subcon Forest Area"), + "Subcon Village - House Top": LocData(2000325471, "Subcon Forest Area"), + "Subcon Village - Ice Cube House": LocData(2000325469, "Subcon Forest Area"), + "Subcon Village - Snatcher Statue Chest": LocData(2000323730, "Subcon Forest Area", paintings=1), + "Subcon Village - Stump Platform Chest": LocData(2000323729, "Subcon Forest Area"), + "Subcon Forest - Giant Tree Climb": LocData(2000325470, "Subcon Forest Area"), + + "Subcon Forest - Ice Cube Shack": LocData(2000324465, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Gravestone": LocData(2000326296, "Subcon Forest Area", + required_hats=[HatType.BREWING], paintings=1), + + "Subcon Forest - Swamp Near Well": LocData(2000324762, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree A": LocData(2000324763, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree B": LocData(2000324764, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Ice Wall": LocData(2000324706, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Treehouse": LocData(2000325468, "Subcon Forest Area", paintings=1), + "Subcon Forest - Swamp Tree Chest": LocData(2000323728, "Subcon Forest Area", paintings=1), + + "Subcon Forest - Burning House": LocData(2000324710, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Tree Climb": LocData(2000325079, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Stump Chest": LocData(2000323731, "Subcon Forest Area", paintings=2), + "Subcon Forest - Burning Forest Treehouse": LocData(2000325467, "Subcon Forest Area", paintings=2), + "Subcon Forest - Spider Bone Cage A": LocData(2000324462, "Subcon Forest Area", paintings=2), + "Subcon Forest - Spider Bone Cage B": LocData(2000325080, "Subcon Forest Area", paintings=2), + "Subcon Forest - Triple Spider Bounce": LocData(2000324765, "Subcon Forest Area", paintings=2), + "Subcon Forest - Noose Treehouse": LocData(2000324856, "Subcon Forest Area", hookshot=True, paintings=2), + + "Subcon Forest - Long Tree Climb Chest": LocData(2000323734, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=2), + + "Subcon Forest - Boss Arena Chest": LocData(2000323735, "Subcon Forest Area"), + "Subcon Forest - Manor Rooftop": LocData(2000325466, "Subcon Forest Area", hit_requirement=2, paintings=1), + + "Subcon Forest - Infinite Yarn Bush": LocData(2000325478, "Subcon Forest Area", + required_hats=[HatType.BREWING], paintings=2), + + "Subcon Forest - Magnet Badge Bush": LocData(2000325479, "Subcon Forest Area", + required_hats=[HatType.BREWING], paintings=3), + + "Subcon Forest - Dweller Stump": LocData(2000324767, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Dweller Floating Rocks": LocData(2000324464, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Dweller Platforming Tree A": LocData(2000324709, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Dweller Platforming Tree B": LocData(2000324855, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Giant Time Piece": LocData(2000325473, "Subcon Forest Area", paintings=3), + "Subcon Forest - Gallows": LocData(2000325472, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Green and Purple Dweller Rocks": LocData(2000325082, "Subcon Forest Area", paintings=3), + + "Subcon Forest - Dweller Shack": LocData(2000324463, "Subcon Forest Area", + required_hats=[HatType.DWELLER], paintings=3), + + "Subcon Forest - Tall Tree Hookshot Swing": LocData(2000324766, "Subcon Forest Area", + required_hats=[HatType.DWELLER], + hookshot=True, + paintings=3), + + "Subcon Well - Hookshot Badge Chest": LocData(2000324114, "The Subcon Well", hit_requirement=1, paintings=1), + "Subcon Well - Above Chest": LocData(2000324612, "The Subcon Well", hit_requirement=1, paintings=1), + "Subcon Well - On Pipe": LocData(2000324311, "The Subcon Well", hookshot=True, hit_requirement=1, paintings=1), + "Subcon Well - Mushroom": LocData(2000325318, "The Subcon Well", hit_requirement=1, paintings=1), + + "Queen Vanessa's Manor - Cellar": LocData(2000324841, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + + "Queen Vanessa's Manor - Bedroom Chest": LocData(2000323808, "Queen Vanessa's Manor", hit_requirement=2, + paintings=1), + + "Queen Vanessa's Manor - Hall Chest": LocData(2000323896, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + "Queen Vanessa's Manor - Chandelier": LocData(2000325546, "Queen Vanessa's Manor", hit_requirement=2, paintings=1), + + # Alpine Skyline + "Alpine Skyline - Goat Village: Below Hookpoint": LocData(2000334856, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Goat Village: Hidden Branch": LocData(2000334855, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area"), + "Alpine Skyline - Bird Pass Fork": LocData(2000335911, "Alpine Skyline Area (TIHS)"), + + "Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)", + required_hats=[HatType.BREWING]), + + "Alpine Skyline - The Purrloined Village: Horned Stone": LocData(2000335561, "Alpine Skyline Area"), + "Alpine Skyline - The Purrloined Village: Chest Reward": LocData(2000334831, "Alpine Skyline Area"), + "Alpine Skyline - The Birdhouse: Triple Crow Chest": LocData(2000334758, "The Birdhouse"), + + "Alpine Skyline - The Birdhouse: Dweller Platforms Relic": LocData(2000336497, "The Birdhouse", + required_hats=[HatType.DWELLER]), + + "Alpine Skyline - The Birdhouse: Brewing Crate House": LocData(2000336496, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Hay Bale": LocData(2000335885, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Alpine Crow Mini-Gauntlet": LocData(2000335886, "The Birdhouse"), + "Alpine Skyline - The Birdhouse: Outer Edge": LocData(2000335492, "The Birdhouse"), + + "Alpine Skyline - Mystifying Time Mesa: Zipline": LocData(2000337058, "Alpine Skyline Area"), + "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": LocData(2000336052, "Alpine Skyline Area"), + "Alpine Skyline - Ember Summit": LocData(2000336311, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - The Lava Cake: Center Fence Cage": LocData(2000335448, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Outer Island Chest": LocData(2000334291, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Dweller Pillars": LocData(2000335417, "The Lava Cake"), + "Alpine Skyline - The Lava Cake: Top Cake": LocData(2000335418, "The Lava Cake"), + "Alpine Skyline - The Twilight Path": LocData(2000334434, "Alpine Skyline Area", required_hats=[HatType.DWELLER]), + "Alpine Skyline - The Twilight Bell: Wide Purple Platform": LocData(2000336478, "The Twilight Bell"), + "Alpine Skyline - The Twilight Bell: Ice Platform": LocData(2000335826, "The Twilight Bell"), + "Alpine Skyline - Goat Outpost Horn": LocData(2000334760, "Alpine Skyline Area"), + "Alpine Skyline - Windy Passage": LocData(2000334776, "Alpine Skyline Area (TIHS)"), + "Alpine Skyline - The Windmill: Inside Pon Cluster": LocData(2000336395, "The Windmill"), + "Alpine Skyline - The Windmill: Entrance": LocData(2000335783, "The Windmill"), + "Alpine Skyline - The Windmill: Dropdown": LocData(2000335815, "The Windmill"), + "Alpine Skyline - The Windmill: House Window": LocData(2000335389, "The Windmill"), + + "The Finale - Frozen Item": LocData(2000304108, "The Finale"), + + "Bon Voyage! - Lamp Post Top": LocData(2000305321, "Bon Voyage!", dlc_flags=HatDLC.dlc1), + "Bon Voyage! - Mafia Cargo Ship": LocData(2000304313, "Bon Voyage!", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Toilet": LocData(2000305109, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Bar": LocData(2000304251, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Dive Board Ledge": LocData(2000304254, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Top Balcony": LocData(2000304255, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Octopus Room": LocData(2000305253, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Octopus Room Top": LocData(2000304249, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Laundry Room": LocData(2000304250, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Ship Side": LocData(2000304247, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "The Arctic Cruise - Silver Ring": LocData(2000305252, "Cruise Ship", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Reception Room - Suitcase": LocData(2000304045, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Reception Room - Under Desk": LocData(2000304047, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Lamp Post": LocData(2000304048, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Iceberg Top": LocData(2000304046, "Rock the Boat", dlc_flags=HatDLC.dlc1), + "Rock the Boat - Post Captain Rescue": LocData(2000304049, "Rock the Boat", dlc_flags=HatDLC.dlc1, + required_hats=[HatType.ICE]), + + "Nyakuza Metro - Main Station Dining Area": LocData(2000304105, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + "Nyakuza Metro - Top of Ramen Shop": LocData(2000304104, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + + "Yellow Overpass Station - Brewing Crate": LocData(2000305413, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.BREWING]), + + "Bluefin Tunnel - Cat Vacuum": LocData(2000305111, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + + "Pink Paw Station - Cat Vacuum": LocData(2000305110, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.DWELLER]), + + "Pink Paw Station - Behind Fan": LocData(2000304106, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.TIME_STOP, HatType.DWELLER]), +} + +act_completions = { + "Act Completion (Time Rift - Gallery)": LocData(2000312758, "Time Rift - Gallery", required_hats=[HatType.BREWING]), + "Act Completion (Time Rift - The Lab)": LocData(2000312838, "Time Rift - The Lab"), + + "Act Completion (Welcome to Mafia Town)": LocData(2000311771, "Welcome to Mafia Town"), + "Act Completion (Barrel Battle)": LocData(2000311958, "Barrel Battle"), + "Act Completion (She Came from Outer Space)": LocData(2000312262, "She Came from Outer Space"), + "Act Completion (Down with the Mafia!)": LocData(2000311326, "Down with the Mafia!"), + "Act Completion (Cheating the Race)": LocData(2000312318, "Cheating the Race", required_hats=[HatType.TIME_STOP]), + "Act Completion (Heating Up Mafia Town)": LocData(2000311481, "Heating Up Mafia Town", umbrella=True), + "Act Completion (The Golden Vault)": LocData(2000312250, "The Golden Vault"), + "Act Completion (Time Rift - Bazaar)": LocData(2000312465, "Time Rift - Bazaar"), + "Act Completion (Time Rift - Sewers)": LocData(2000312484, "Time Rift - Sewers"), + "Act Completion (Time Rift - Mafia of Cooks)": LocData(2000311855, "Time Rift - Mafia of Cooks"), + + "Act Completion (Dead Bird Studio)": LocData(2000311383, "Dead Bird Studio", hit_requirement=1), + "Act Completion (Murder on the Owl Express)": LocData(2000311544, "Murder on the Owl Express"), + "Act Completion (Picture Perfect)": LocData(2000311587, "Picture Perfect"), + "Act Completion (Train Rush)": LocData(2000312481, "Train Rush", hookshot=True), + "Act Completion (The Big Parade)": LocData(2000311157, "The Big Parade", umbrella=True), + "Act Completion (Award Ceremony)": LocData(2000311488, "Award Ceremony"), + "Act Completion (Dead Bird Studio Basement)": LocData(2000312253, "Dead Bird Studio Basement", hookshot=True), + "Act Completion (Time Rift - The Owl Express)": LocData(2000312807, "Time Rift - The Owl Express"), + "Act Completion (Time Rift - The Moon)": LocData(2000312785, "Time Rift - The Moon"), + "Act Completion (Time Rift - Dead Bird Studio)": LocData(2000312577, "Time Rift - Dead Bird Studio"), + + "Act Completion (Contractual Obligations)": LocData(2000312317, "Contractual Obligations", paintings=1), + + "Act Completion (The Subcon Well)": LocData(2000311160, "The Subcon Well", hookshot=True, hit_requirement=1, + paintings=1), + + "Act Completion (Toilet of Doom)": LocData(2000311984, "Toilet of Doom", hit_requirement=1, hookshot=True, + paintings=1), + + "Act Completion (Queen Vanessa's Manor)": LocData(2000312017, "Queen Vanessa's Manor", umbrella=True, paintings=1), + + "Act Completion (Mail Delivery Service)": LocData(2000312032, "Mail Delivery Service", + required_hats=[HatType.SPRINT]), + + "Act Completion (Your Contract has Expired)": LocData(2000311390, "Your Contract has Expired", umbrella=True), + "Act Completion (Time Rift - Pipe)": LocData(2000313069, "Time Rift - Pipe", hookshot=True), + "Act Completion (Time Rift - Village)": LocData(2000313056, "Time Rift - Village"), + "Act Completion (Time Rift - Sleepy Subcon)": LocData(2000312086, "Time Rift - Sleepy Subcon"), + + "Act Completion (The Birdhouse)": LocData(2000311428, "The Birdhouse"), + "Act Completion (The Lava Cake)": LocData(2000312509, "The Lava Cake"), + "Act Completion (The Twilight Bell)": LocData(2000311540, "The Twilight Bell"), + "Act Completion (The Windmill)": LocData(2000312263, "The Windmill"), + "Act Completion (The Illness has Spread)": LocData(2000312022, "The Illness has Spread", hookshot=True), + + "Act Completion (Time Rift - The Twilight Bell)": LocData(2000312399, "Time Rift - The Twilight Bell", + required_hats=[HatType.DWELLER]), + + "Act Completion (Time Rift - Curly Tail Trail)": LocData(2000313335, "Time Rift - Curly Tail Trail", + required_hats=[HatType.ICE]), + + "Act Completion (Time Rift - Alpine Skyline)": LocData(2000311777, "Time Rift - Alpine Skyline"), + + "Act Completion (The Finale)": LocData(2000311872, "The Finale", hookshot=True, required_hats=[HatType.DWELLER]), + "Act Completion (Time Rift - Tour)": LocData(2000311803, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + + "Act Completion (Bon Voyage!)": LocData(2000311520, "Bon Voyage!", dlc_flags=HatDLC.dlc1, hookshot=True), + "Act Completion (Ship Shape)": LocData(2000311451, "Ship Shape", dlc_flags=HatDLC.dlc1), + + "Act Completion (Rock the Boat)": LocData(2000311437, "Rock the Boat", dlc_flags=HatDLC.dlc1, + required_hats=[HatType.ICE]), + + "Act Completion (Time Rift - Balcony)": LocData(2000312226, "Time Rift - Balcony", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Act Completion (Time Rift - Deep Sea)": LocData(2000312434, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True, required_hats=[HatType.DWELLER, HatType.ICE]), + + "Act Completion (Nyakuza Metro Intro)": LocData(2000311138, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + + "Act Completion (Yellow Overpass Station)": LocData(2000311206, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2, + hookshot=True), + + "Act Completion (Yellow Overpass Manhole)": LocData(2000311387, "Yellow Overpass Manhole", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE]), + + "Act Completion (Green Clean Station)": LocData(2000311207, "Green Clean Station", dlc_flags=HatDLC.dlc2), + + "Act Completion (Green Clean Manhole)": LocData(2000311388, "Green Clean Manhole", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE, HatType.DWELLER]), + + "Act Completion (Bluefin Tunnel)": LocData(2000311208, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + + "Act Completion (Pink Paw Station)": LocData(2000311209, "Pink Paw Station", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.DWELLER]), + + "Act Completion (Pink Paw Manhole)": LocData(2000311389, "Pink Paw Manhole", + dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE]), + + "Act Completion (Rush Hour)": LocData(2000311210, "Rush Hour", + dlc_flags=HatDLC.dlc2, + hookshot=True, + required_hats=[HatType.ICE, HatType.BREWING]), + + "Act Completion (Time Rift - Rumbi Factory)": LocData(2000312736, "Time Rift - Rumbi Factory", + dlc_flags=HatDLC.dlc2), +} + +storybook_pages = { + "Mafia of Cooks - Page: Fish Pile": LocData(2000345091, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Trash Mound": LocData(2000345090, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Beside Red Building": LocData(2000345092, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Behind Shipping Containers": LocData(2000345095, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Top of Boat": LocData(2000345093, "Time Rift - Mafia of Cooks"), + "Mafia of Cooks - Page: Below Dock": LocData(2000345094, "Time Rift - Mafia of Cooks"), + + "Dead Bird Studio (Rift) - Page: Behind Cardboard Planet": LocData(2000345449, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Near Time Rift Gate": LocData(2000345447, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Top of Metal Bar": LocData(2000345448, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Lava Lamp": LocData(2000345450, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Above Horse Picture": LocData(2000345451, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Green Screen": LocData(2000345452, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: In The Corner": LocData(2000345453, "Time Rift - Dead Bird Studio"), + "Dead Bird Studio (Rift) - Page: Above TV Room": LocData(2000345445, "Time Rift - Dead Bird Studio"), + + "Sleepy Subcon - Page: Behind Entrance Area": LocData(2000345373, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Near Wrecking Ball": LocData(2000345327, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind Crane": LocData(2000345371, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Wrecked Treehouse": LocData(2000345326, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind 2nd Rift Gate": LocData(2000345372, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Rotating Platform": LocData(2000345328, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Behind 3rd Rift Gate": LocData(2000345329, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Frozen Tree": LocData(2000345330, "Time Rift - Sleepy Subcon"), + "Sleepy Subcon - Page: Secret Library": LocData(2000345370, "Time Rift - Sleepy Subcon"), + + "Alpine Skyline (Rift) - Page: Entrance Area Hidden Ledge": LocData(2000345016, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Windmill Island Ledge": LocData(2000345012, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Waterfall Wooden Pillar": LocData(2000345015, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Lonely Birdhouse Top": LocData(2000345014, "Time Rift - Alpine Skyline"), + "Alpine Skyline (Rift) - Page: Below Aqueduct": LocData(2000345013, "Time Rift - Alpine Skyline"), + + "Deep Sea - Page: Starfish": LocData(2000346454, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + "Deep Sea - Page: Mini Castle": LocData(2000346452, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + "Deep Sea - Page: Urchins": LocData(2000346449, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1), + + "Deep Sea - Page: Big Castle": LocData(2000346450, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Castle Top Chest": LocData(2000304850, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Urchin Ledge": LocData(2000346451, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Hidden Castle Chest": LocData(2000304849, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Falling Platform": LocData(2000346456, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Deep Sea - Page: Lava Starfish": LocData(2000346453, "Time Rift - Deep Sea", dlc_flags=HatDLC.dlc1, + hookshot=True), + + "Tour - Page: Mafia Town - Ledge": LocData(2000345038, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Mafia Town - Beach": LocData(2000345039, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Dead Bird Studio - C.A.W. Agents": LocData(2000345040, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Dead Bird Studio - Fragile Box": LocData(2000345041, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Subcon Forest - Giant Frozen Tree": LocData(2000345042, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Subcon Forest - Top of Pillar": LocData(2000345043, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Alpine Skyline - Birdhouse": LocData(2000345044, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: Alpine Skyline - Behind Lava Isle": LocData(2000345047, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + "Tour - Page: The Finale - Near Entrance": LocData(2000345087, "Time Rift - Tour", dlc_flags=HatDLC.dlc1), + + "Rumbi Factory - Page: Manhole": LocData(2000345891, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Shutter Doors": LocData(2000345888, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: Toxic Waste Dispenser": LocData(2000345892, "Time Rift - Rumbi Factory", + dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: 3rd Area Ledge": LocData(2000345889, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: Green Box Assembly Line": LocData(2000345884, "Time Rift - Rumbi Factory", + dlc_flags=HatDLC.dlc2), + + "Rumbi Factory - Page: Broken Window": LocData(2000345885, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Money Vault": LocData(2000345890, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Warehouse Boxes": LocData(2000345887, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Glass Shelf": LocData(2000345886, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), + "Rumbi Factory - Page: Last Area": LocData(2000345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2), +} + +shop_locations = { + "Badge Seller - Item 1": LocData(2000301003, "Badge Seller"), + "Badge Seller - Item 2": LocData(2000301004, "Badge Seller"), + "Badge Seller - Item 3": LocData(2000301005, "Badge Seller"), + "Badge Seller - Item 4": LocData(2000301006, "Badge Seller"), + "Badge Seller - Item 5": LocData(2000301007, "Badge Seller"), + "Badge Seller - Item 6": LocData(2000301008, "Badge Seller"), + "Badge Seller - Item 7": LocData(2000301009, "Badge Seller"), + "Badge Seller - Item 8": LocData(2000301010, "Badge Seller"), + "Badge Seller - Item 9": LocData(2000301011, "Badge Seller"), + "Badge Seller - Item 10": LocData(2000301012, "Badge Seller"), + "Mafia Boss Shop Item": LocData(2000301013, "Spaceship"), + + "Yellow Overpass Station - Yellow Ticket Booth": LocData(2000301014, "Yellow Overpass Station", + dlc_flags=HatDLC.dlc2), + + "Green Clean Station - Green Ticket Booth": LocData(2000301015, "Green Clean Station", dlc_flags=HatDLC.dlc2), + "Bluefin Tunnel - Blue Ticket Booth": LocData(2000301016, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2), + + "Pink Paw Station - Pink Ticket Booth": LocData(2000301017, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + hookshot=True, required_hats=[HatType.DWELLER]), + + "Main Station Thug A - Item 1": LocData(2000301048, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 2": LocData(2000301049, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 3": LocData(2000301050, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 4": LocData(2000301051, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + "Main Station Thug A - Item 5": LocData(2000301052, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_0"), + + "Main Station Thug B - Item 1": LocData(2000301053, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 2": LocData(2000301054, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 3": LocData(2000301055, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 4": LocData(2000301056, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + "Main Station Thug B - Item 5": LocData(2000301057, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_1"), + + "Main Station Thug C - Item 1": LocData(2000301058, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 2": LocData(2000301059, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 3": LocData(2000301060, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 4": LocData(2000301061, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + "Main Station Thug C - Item 5": LocData(2000301062, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_2"), + + "Yellow Overpass Thug A - Item 1": LocData(2000301018, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 2": LocData(2000301019, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 3": LocData(2000301020, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 4": LocData(2000301021, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + "Yellow Overpass Thug A - Item 5": LocData(2000301022, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_13"), + + "Yellow Overpass Thug B - Item 1": LocData(2000301043, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 2": LocData(2000301044, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 3": LocData(2000301045, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 4": LocData(2000301046, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + "Yellow Overpass Thug B - Item 5": LocData(2000301047, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_5"), + + "Yellow Overpass Thug C - Item 1": LocData(2000301063, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 2": LocData(2000301064, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 3": LocData(2000301065, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 4": LocData(2000301066, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + "Yellow Overpass Thug C - Item 5": LocData(2000301067, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_14"), + + "Green Clean Station Thug A - Item 1": LocData(2000301033, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 2": LocData(2000301034, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 3": LocData(2000301035, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 4": LocData(2000301036, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + "Green Clean Station Thug A - Item 5": LocData(2000301037, "Green Clean Station", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_4"), + + # This guy requires either the yellow ticket or the Ice Hat + "Green Clean Station Thug B - Item 1": LocData(2000301028, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 2": LocData(2000301029, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 3": LocData(2000301030, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 4": LocData(2000301031, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + "Green Clean Station Thug B - Item 5": LocData(2000301032, "Green Clean Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.ICE], nyakuza_thug="Hat_NPC_NyakuzaShop_6"), + + "Bluefin Tunnel Thug - Item 1": LocData(2000301023, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 2": LocData(2000301024, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 3": LocData(2000301025, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 4": LocData(2000301026, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + "Bluefin Tunnel Thug - Item 5": LocData(2000301027, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, + nyakuza_thug="Hat_NPC_NyakuzaShop_7"), + + "Pink Paw Station Thug - Item 1": LocData(2000301038, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 2": LocData(2000301039, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 3": LocData(2000301040, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 4": LocData(2000301041, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + "Pink Paw Station Thug - Item 5": LocData(2000301042, "Pink Paw Station", dlc_flags=HatDLC.dlc2, + required_hats=[HatType.DWELLER], hookshot=True, + nyakuza_thug="Hat_NPC_NyakuzaShop_12"), + +} + +contract_locations = { + "Snatcher's Contract - The Subcon Well": LocData(2000300200, "Contractual Obligations"), + "Snatcher's Contract - Toilet of Doom": LocData(2000300201, "Subcon Forest Area"), + "Snatcher's Contract - Queen Vanessa's Manor": LocData(2000300202, "Subcon Forest Area"), + "Snatcher's Contract - Mail Delivery Service": LocData(2000300203, "Subcon Forest Area"), +} + +# Don't put any of the locations from peaks here, the rules for their entrances are set already +zipline_unlocks = { + "Alpine Skyline - Bird Pass Fork": "Zipline Unlock - The Birdhouse Path", + "Alpine Skyline - Yellow Band Hills": "Zipline Unlock - The Birdhouse Path", + "Alpine Skyline - The Purrloined Village: Horned Stone": "Zipline Unlock - The Birdhouse Path", + "Alpine Skyline - The Purrloined Village: Chest Reward": "Zipline Unlock - The Birdhouse Path", + + "Alpine Skyline - Mystifying Time Mesa: Zipline": "Zipline Unlock - The Lava Cake Path", + "Alpine Skyline - Mystifying Time Mesa: Gate Puzzle": "Zipline Unlock - The Lava Cake Path", + "Alpine Skyline - Ember Summit": "Zipline Unlock - The Lava Cake Path", + + "Alpine Skyline - Goat Outpost Horn": "Zipline Unlock - The Windmill Path", + "Alpine Skyline - Windy Passage": "Zipline Unlock - The Windmill Path", + + "Alpine Skyline - The Twilight Path": "Zipline Unlock - The Twilight Bell Path", +} + +# act completion rules should be set automatically as these are all event items +zero_jumps_hard = { + "Time Rift - Sewers (Zero Jumps)": LocData(0, "Time Rift - Sewers", + required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + + "Time Rift - Bazaar (Zero Jumps)": LocData(0, "Time Rift - Bazaar", + required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + + "The Big Parade": LocData(0, "The Big Parade", + umbrella=True, + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Time Rift - Pipe (Zero Jumps)": LocData(0, "Time Rift - Pipe", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Time Rift - Curly Tail Trail (Zero Jumps)": LocData(0, "Time Rift - Curly Tail Trail", + required_hats=[HatType.ICE], dlc_flags=HatDLC.death_wish), + + "Time Rift - The Twilight Bell (Zero Jumps)": LocData(0, "Time Rift - The Twilight Bell", + required_hats=[HatType.ICE, HatType.DWELLER], + hit_requirement=1, + dlc_flags=HatDLC.death_wish), + + "The Illness has Spread (Zero Jumps)": LocData(0, "The Illness has Spread", + required_hats=[HatType.ICE], hookshot=True, + hit_requirement=1, dlc_flags=HatDLC.death_wish), + + "The Finale (Zero Jumps)": LocData(0, "The Finale", + required_hats=[HatType.ICE, HatType.DWELLER], + hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Pink Paw Station (Zero Jumps)": LocData(0, "Pink Paw Station", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.dlc2_dw), +} + +zero_jumps_expert = { + "The Birdhouse (Zero Jumps)": LocData(0, "The Birdhouse", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "The Lava Cake (Zero Jumps)": LocData(0, "The Lava Cake", dlc_flags=HatDLC.death_wish), + + "The Windmill (Zero Jumps)": LocData(0, "The Windmill", + required_hats=[HatType.ICE], + misc_required=["No Bonk Badge"], + dlc_flags=HatDLC.death_wish), + "The Twilight Bell (Zero Jumps)": LocData(0, "The Twilight Bell", + required_hats=[HatType.ICE, HatType.DWELLER], + hit_requirement=1, + misc_required=["No Bonk Badge"], + dlc_flags=HatDLC.death_wish), + + "Sleepy Subcon (Zero Jumps)": LocData(0, "Time Rift - Sleepy Subcon", required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Ship Shape (Zero Jumps)": LocData(0, "Ship Shape", required_hats=[HatType.ICE], dlc_flags=HatDLC.dlc1_dw), +} + +zero_jumps = { + **zero_jumps_hard, + **zero_jumps_expert, + "Welcome to Mafia Town (Zero Jumps)": LocData(0, "Welcome to Mafia Town", dlc_flags=HatDLC.death_wish), + + "Down with the Mafia! (Zero Jumps)": LocData(0, "Down with the Mafia!", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Cheating the Race (Zero Jumps)": LocData(0, "Cheating the Race", + required_hats=[HatType.TIME_STOP], + dlc_flags=HatDLC.death_wish), + + "The Golden Vault (Zero Jumps)": LocData(0, "The Golden Vault", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Dead Bird Studio (Zero Jumps)": LocData(0, "Dead Bird Studio", + required_hats=[HatType.ICE], + hit_requirement=1, + dlc_flags=HatDLC.death_wish), + + "Murder on the Owl Express (Zero Jumps)": LocData(0, "Murder on the Owl Express", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Picture Perfect (Zero Jumps)": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), + + "Train Rush (Zero Jumps)": LocData(0, "Train Rush", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Contractual Obligations (Zero Jumps)": LocData(0, "Contractual Obligations", + paintings=1, + dlc_flags=HatDLC.death_wish), + + "Your Contract has Expired (Zero Jumps)": LocData(0, "Your Contract has Expired", + umbrella=True, + dlc_flags=HatDLC.death_wish), + + # No ice hat/painting required in Expert + "Toilet of Doom (Zero Jumps)": LocData(0, "Toilet of Doom", + hookshot=True, + hit_requirement=1, + required_hats=[HatType.ICE], + paintings=1, + dlc_flags=HatDLC.death_wish), + + "Mail Delivery Service (Zero Jumps)": LocData(0, "Mail Delivery Service", + required_hats=[HatType.SPRINT], + dlc_flags=HatDLC.death_wish), + + "Time Rift - Alpine Skyline (Zero Jumps)": LocData(0, "Time Rift - Alpine Skyline", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Time Rift - The Lab (Zero Jumps)": LocData(0, "Time Rift - The Lab", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.death_wish), + + "Yellow Overpass Station (Zero Jumps)": LocData(0, "Yellow Overpass Station", + required_hats=[HatType.ICE], + hookshot=True, + dlc_flags=HatDLC.dlc2_dw), + + "Green Clean Station (Zero Jumps)": LocData(0, "Green Clean Station", + required_hats=[HatType.ICE], + dlc_flags=HatDLC.dlc2_dw), +} + +# noinspection PyDictDuplicateKeys +snatcher_coins = { + "Snatcher Coin - Top of HQ": LocData(0, "Down with the Mafia!", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "Heating Up Mafia Town", umbrella=True, dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "The Golden Vault", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of HQ": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Tower": LocData(0, "Mafia Town Area (HUMT)", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "Beat the Heat", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "Collect-a-thon", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "She Speedran from Outer Space", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Tower": LocData(0, "Mafia's Jumps", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Under Ruined Tower": LocData(0, "Mafia Town Area", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Under Ruined Tower": LocData(0, "Collect-a-thon", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Under Ruined Tower": LocData(0, "She Speedran from Outer Space", dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Top of Red House": LocData(0, "Dead Bird Studio - Elevator Area", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Top of Red House": LocData(0, "Security Breach", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Train Rush": LocData(0, "Train Rush", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Train Rush": LocData(0, "10 Seconds until Self-Destruct", hookshot=True, + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Picture Perfect": LocData(0, "Picture Perfect", dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Swamp Tree": LocData(0, "Subcon Forest Area", hookshot=True, paintings=1, + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Swamp Tree": LocData(0, "Speedrun Well", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Manor Roof": LocData(0, "Subcon Forest Area", hit_requirement=2, paintings=1, + dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Giant Time Piece": LocData(0, "Subcon Forest Area", paintings=3, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Goat Village Top": LocData(0, "Alpine Skyline Area (TIHS)", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Goat Village Top": LocData(0, "The Illness has Speedrun", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Lava Cake": LocData(0, "The Lava Cake", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Windmill": LocData(0, "The Windmill", dlc_flags=HatDLC.death_wish), + "Snatcher Coin - Windmill": LocData(0, "Wound-Up Windmill", hookshot=True, dlc_flags=HatDLC.death_wish), + + "Snatcher Coin - Green Clean Tower": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2_dw), + "Snatcher Coin - Bluefin Cat Train": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2_dw), + "Snatcher Coin - Pink Paw Fence": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2_dw), +} + +event_locs = { + **zero_jumps, + **snatcher_coins, + "HUMT Access": LocData(0, "Heating Up Mafia Town"), + "TOD Access": LocData(0, "Toilet of Doom"), + "YCHE Access": LocData(0, "Your Contract has Expired"), + + "Birdhouse Cleared": LocData(0, "The Birdhouse", act_event=True), + "Lava Cake Cleared": LocData(0, "The Lava Cake", act_event=True), + "Windmill Cleared": LocData(0, "The Windmill", act_event=True), + "Twilight Bell Cleared": LocData(0, "The Twilight Bell", act_event=True), + "Time Piece Cluster": LocData(0, "The Finale", act_event=True), + + # not really an act + "Nyakuza Intro Cleared": LocData(0, "Nyakuza Free Roam", dlc_flags=HatDLC.dlc2), + + "Yellow Overpass Station Cleared": LocData(0, "Yellow Overpass Station", dlc_flags=HatDLC.dlc2, act_event=True), + "Green Clean Station Cleared": LocData(0, "Green Clean Station", dlc_flags=HatDLC.dlc2, act_event=True), + "Bluefin Tunnel Cleared": LocData(0, "Bluefin Tunnel", dlc_flags=HatDLC.dlc2, act_event=True), + "Pink Paw Station Cleared": LocData(0, "Pink Paw Station", dlc_flags=HatDLC.dlc2, act_event=True), + "Yellow Overpass Manhole Cleared": LocData(0, "Yellow Overpass Manhole", dlc_flags=HatDLC.dlc2, act_event=True), + "Green Clean Manhole Cleared": LocData(0, "Green Clean Manhole", dlc_flags=HatDLC.dlc2, act_event=True), + "Pink Paw Manhole Cleared": LocData(0, "Pink Paw Manhole", dlc_flags=HatDLC.dlc2, act_event=True), + "Rush Hour Cleared": LocData(0, "Rush Hour", dlc_flags=HatDLC.dlc2, act_event=True), +} + +# DO NOT ALTER THE ORDER OF THIS LIST +death_wishes = { + "Beat the Heat": 2000350000, + "Snatcher's Hit List": 2000350002, + "So You're Back From Outer Space": 2000350004, + "Collect-a-thon": 2000350006, + "Rift Collapse: Mafia of Cooks": 2000350008, + "She Speedran from Outer Space": 2000350010, + "Mafia's Jumps": 2000350012, + "Vault Codes in the Wind": 2000350014, + "Encore! Encore!": 2000350016, + "Snatcher Coins in Mafia Town": 2000350018, + + "Security Breach": 2000350020, + "The Great Big Hootenanny": 2000350022, + "Rift Collapse: Dead Bird Studio": 2000350024, + "10 Seconds until Self-Destruct": 2000350026, + "Killing Two Birds": 2000350028, + "Snatcher Coins in Battle of the Birds": 2000350030, + "Zero Jumps": 2000350032, + + "Speedrun Well": 2000350034, + "Rift Collapse: Sleepy Subcon": 2000350036, + "Boss Rush": 2000350038, + "Quality Time with Snatcher": 2000350040, + "Breaching the Contract": 2000350042, + "Snatcher Coins in Subcon Forest": 2000350044, + + "Bird Sanctuary": 2000350046, + "Rift Collapse: Alpine Skyline": 2000350048, + "Wound-Up Windmill": 2000350050, + "The Illness has Speedrun": 2000350052, + "Snatcher Coins in Alpine Skyline": 2000350054, + "Camera Tourist": 2000350056, + + "The Mustache Gauntlet": 2000350058, + "No More Bad Guys": 2000350060, + + "Seal the Deal": 2000350062, + "Rift Collapse: Deep Sea": 2000350064, + "Cruisin' for a Bruisin'": 2000350066, + + "Community Rift: Rhythm Jump Studio": 2000350068, + "Community Rift: Twilight Travels": 2000350070, + "Community Rift: The Mountain Rift": 2000350072, + "Snatcher Coins in Nyakuza Metro": 2000350074, +} + +location_table = { + **ahit_locations, + **act_completions, + **storybook_pages, + **contract_locations, + **shop_locations, +} diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py new file mode 100644 index 0000000000..f3dd2a8c66 --- /dev/null +++ b/worlds/ahit/Options.py @@ -0,0 +1,728 @@ +import typing +from worlds.AutoWorld import World +from Options import Option, Range, Toggle, DeathLink, Choice, OptionDict + + +def adjust_options(world: World): + world.multiworld.HighestChapterCost[world.player].value = max( + world.multiworld.HighestChapterCost[world.player].value, + world.multiworld.LowestChapterCost[world.player].value) + + world.multiworld.LowestChapterCost[world.player].value = min( + world.multiworld.LowestChapterCost[world.player].value, + world.multiworld.HighestChapterCost[world.player].value) + + world.multiworld.FinalChapterMinCost[world.player].value = min( + world.multiworld.FinalChapterMinCost[world.player].value, + world.multiworld.FinalChapterMaxCost[world.player].value) + + world.multiworld.FinalChapterMaxCost[world.player].value = max( + world.multiworld.FinalChapterMaxCost[world.player].value, + world.multiworld.FinalChapterMinCost[world.player].value) + + world.multiworld.BadgeSellerMinItems[world.player].value = min( + world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) + + world.multiworld.BadgeSellerMaxItems[world.player].value = max( + world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) + + world.multiworld.NyakuzaThugMinShopItems[world.player].value = min( + world.multiworld.NyakuzaThugMinShopItems[world.player].value, + world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + + world.multiworld.NyakuzaThugMaxShopItems[world.player].value = max( + world.multiworld.NyakuzaThugMinShopItems[world.player].value, + world.multiworld.NyakuzaThugMaxShopItems[world.player].value) + + world.multiworld.DWShuffleCountMin[world.player].value = min( + world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + world.multiworld.DWShuffleCountMax[world.player].value = max( + world.multiworld.DWShuffleCountMin[world.player].value, + world.multiworld.DWShuffleCountMax[world.player].value) + + total_tps: int = get_total_time_pieces(world) + if world.multiworld.HighestChapterCost[world.player].value > total_tps-5: + world.multiworld.HighestChapterCost[world.player].value = min(45, total_tps-5) + + if world.multiworld.LowestChapterCost[world.player].value > total_tps-5: + world.multiworld.LowestChapterCost[world.player].value = min(45, total_tps-5) + + if world.multiworld.FinalChapterMaxCost[world.player].value > total_tps: + world.multiworld.FinalChapterMaxCost[world.player].value = min(50, total_tps) + + if world.multiworld.FinalChapterMinCost[world.player].value > total_tps: + world.multiworld.FinalChapterMinCost[world.player].value = min(50, total_tps-5) + + # Don't allow Rush Hour goal if DLC2 content is disabled + if world.multiworld.EndGoal[world.player].value == 2 and world.multiworld.EnableDLC2[world.player].value == 0: + world.multiworld.EndGoal[world.player].value = 1 + + # Don't allow Seal the Deal goal if Death Wish content is disabled + if world.multiworld.EndGoal[world.player].value == 3 and not world.is_dw(): + world.multiworld.EndGoal[world.player].value = 1 + + if world.multiworld.DWEnableBonus[world.player].value > 0: + world.multiworld.DWAutoCompleteBonuses[world.player].value = 0 + + if world.is_dw_only(): + world.multiworld.EndGoal[world.player].value = 3 + world.multiworld.ActRandomizer[world.player].value = 0 + world.multiworld.ShuffleAlpineZiplines[world.player].value = 0 + world.multiworld.ShuffleSubconPaintings[world.player].value = 0 + world.multiworld.ShuffleStorybookPages[world.player].value = 0 + world.multiworld.ShuffleActContracts[world.player].value = 0 + world.multiworld.EnableDLC1[world.player].value = 0 + world.multiworld.LogicDifficulty[world.player].value = -1 + world.multiworld.DWTimePieceRequirement[world.player].value = 0 + + +def get_total_time_pieces(world: World) -> int: + count: int = 40 + if world.is_dlc1(): + count += 6 + + if world.is_dlc2(): + count += 10 + + return min(40+world.multiworld.MaxExtraTimePieces[world.player].value, count) + + +class EndGoal(Choice): + """The end goal required to beat the game. + Finale: Reach Time's End and beat Mustache Girl. The Finale will be in its vanilla location. + + Rush Hour: Reach and complete Rush Hour. The level will be in its vanilla location and Chapter 7 + will be the final chapter. You also must find Nyakuza Metro itself and complete all of its levels. + Requires DLC2 content to be enabled. + + Seal the Deal: Reach and complete the Seal the Deal death wish main objective. + Requires Death Wish content to be enabled.""" + display_name = "End Goal" + option_finale = 1 + option_rush_hour = 2 + option_seal_the_deal = 3 + default = 1 + + +class ActRandomizer(Choice): + """If enabled, shuffle the game's Acts between each other. + Light will cause Time Rifts to only be shuffled amongst each other, + and Blue Time Rifts and Purple Time Rifts to be shuffled separately.""" + display_name = "Shuffle Acts" + option_false = 0 + option_light = 1 + option_insanity = 2 + default = 1 + + +class ActPlando(OptionDict): + """Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\"""" + display_name = "Act Plando" + + +class FinaleShuffle(Toggle): + """If enabled, chapter finales will only be shuffled amongst each other in act shuffle.""" + display_name = "Finale Shuffle" + default = 0 + + +class LogicDifficulty(Choice): + """Choose the difficulty setting for logic.""" + display_name = "Logic Difficulty" + option_normal = -1 + option_moderate = 0 + option_hard = 1 + option_expert = 2 + default = -1 + + +class CTRLogic(Choice): + """Choose how you want to logically clear Cheating the Race.""" + display_name = "Cheating the Race Logic" + option_time_stop_only = 0 + option_scooter = 1 + option_sprint = 2 + option_nothing = 3 + default = 0 + + +class RandomizeHatOrder(Choice): + """Randomize the order that hats are stitched in. + Time Stop Last will force Time Stop to be the last hat in the sequence.""" + display_name = "Randomize Hat Order" + option_false = 0 + option_true = 1 + option_time_stop_last = 2 + default = 1 + + +class YarnBalancePercent(Range): + """How much (in percentage) of the yarn in the pool that will be progression balanced.""" + display_name = "Yarn Balance Percentage" + default = 20 + range_start = 0 + range_end = 100 + + +class TimePieceBalancePercent(Range): + """How much (in percentage) of time pieces in the pool that will be progression balanced.""" + display_name = "Time Piece Balance Percentage" + default = 35 + range_start = 0 + range_end = 100 + + +class StartWithCompassBadge(Toggle): + """If enabled, start with the Compass Badge. In Archipelago, the Compass Badge will track all items in the world + (instead of just Relics). Recommended if you're not familiar with where item locations are.""" + display_name = "Start with Compass Badge" + default = 1 + + +class CompassBadgeMode(Choice): + """closest - Compass Badge points to the closest item regardless of classification + important_only - Compass Badge points to progression/useful items only + important_first - Compass Badge points to progression/useful items first, then it will point to junk items""" + display_name = "Compass Badge Mode" + option_closest = 1 + option_important_only = 2 + option_important_first = 3 + default = 1 + + +class UmbrellaLogic(Toggle): + """Makes Hat Kid's default punch attack do absolutely nothing, making the Umbrella much more relevant and useful""" + display_name = "Umbrella Logic" + default = 0 + + +class ShuffleStorybookPages(Toggle): + """If enabled, each storybook page in the purple Time Rifts is an item check. + The Compass Badge can track these down for you.""" + display_name = "Shuffle Storybook Pages" + default = 1 + + +class ShuffleActContracts(Toggle): + """If enabled, shuffle Snatcher's act contracts into the pool as items""" + display_name = "Shuffle Contracts" + default = 1 + + +class ShuffleAlpineZiplines(Toggle): + """If enabled, Alpine's zipline paths leading to the peaks will be locked behind items.""" + display_name = "Shuffle Alpine Ziplines" + default = 0 + + +class ShuffleSubconPaintings(Toggle): + """If enabled, shuffle items into the pool that unlock Subcon Forest fire spirit paintings. + These items are progressive, with the order of Village-Swamp-Courtyard.""" + display_name = "Shuffle Subcon Paintings" + default = 0 + + +class NoPaintingSkips(Toggle): + """If enabled, prevent Subcon fire wall skips from being in logic on higher difficulty settings.""" + display_name = "No Subcon Fire Wall Skips" + default = 0 + + +class StartingChapter(Choice): + """Determines which chapter you will be guaranteed to be able to enter at the beginning of the game.""" + display_name = "Starting Chapter" + option_1 = 1 + option_2 = 2 + option_3 = 3 + option_4 = 4 + default = 1 + + +class ChapterCostIncrement(Range): + """Lower values mean chapter costs increase slower. Higher values make the cost differences more steep.""" + display_name = "Chapter Cost Increment" + range_start = 1 + range_end = 8 + default = 4 + + +class ChapterCostMinDifference(Range): + """The minimum difference between chapter costs.""" + display_name = "Minimum Chapter Cost Difference" + range_start = 1 + range_end = 8 + default = 4 + + +class LowestChapterCost(Range): + """Value determining the lowest possible cost for a chapter. + Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" + display_name = "Lowest Possible Chapter Cost" + range_start = 0 + range_end = 10 + default = 5 + + +class HighestChapterCost(Range): + """Value determining the highest possible cost for a chapter. + Chapter costs will, progressively, be calculated based on this value (except for the final chapter).""" + display_name = "Highest Possible Chapter Cost" + range_start = 15 + range_end = 45 + default = 25 + + +class FinalChapterMinCost(Range): + """Minimum Time Pieces required to enter the final chapter. This is part of your goal.""" + display_name = "Final Chapter Minimum Time Piece Cost" + range_start = 0 + range_end = 50 + default = 30 + + +class FinalChapterMaxCost(Range): + """Maximum Time Pieces required to enter the final chapter. This is part of your goal.""" + display_name = "Final Chapter Maximum Time Piece Cost" + range_start = 0 + range_end = 50 + default = 35 + + +class MaxExtraTimePieces(Range): + """Maximum amount of extra Time Pieces from the DLCs. + Arctic Cruise will add up to 6. Nyakuza Metro will add up to 10. The absolute maximum is 56.""" + display_name = "Max Extra Time Pieces" + range_start = 0 + range_end = 16 + default = 16 + + +class YarnCostMin(Range): + """The minimum possible yarn needed to stitch a hat.""" + display_name = "Minimum Yarn Cost" + range_start = 1 + range_end = 12 + default = 4 + + +class YarnCostMax(Range): + """The maximum possible yarn needed to stitch a hat.""" + display_name = "Maximum Yarn Cost" + range_start = 1 + range_end = 12 + default = 8 + + +class YarnAvailable(Range): + """How much yarn is available to collect in the item pool.""" + display_name = "Yarn Available" + range_start = 30 + range_end = 80 + default = 50 + + +class MinExtraYarn(Range): + """The minimum amount of extra yarn in the item pool. + There must be at least this much more yarn over the total amount of yarn needed to craft all hats. + For example, if this option's value is 10, and the total yarn needed to craft all hats is 40, + there must be at least 50 yarn in the pool.""" + display_name = "Max Extra Yarn" + range_start = 5 + range_end = 15 + default = 10 + + +class HatItems(Toggle): + """Removes all yarn from the pool and turns the hats into individual items instead.""" + display_name = "Hat Items" + default = 0 + + +class MinPonCost(Range): + """The minimum amount of Pons that any shop item can cost.""" + display_name = "Minimum Shop Pon Cost" + range_start = 10 + range_end = 800 + default = 75 + + +class MaxPonCost(Range): + """The maximum amount of Pons that any shop item can cost.""" + display_name = "Maximum Shop Pon Cost" + range_start = 10 + range_end = 800 + default = 300 + + +class BadgeSellerMinItems(Range): + """The smallest amount of items that the Badge Seller can have for sale.""" + display_name = "Badge Seller Minimum Items" + range_start = 0 + range_end = 10 + default = 4 + + +class BadgeSellerMaxItems(Range): + """The largest amount of items that the Badge Seller can have for sale.""" + display_name = "Badge Seller Maximum Items" + range_start = 0 + range_end = 10 + default = 8 + + +class EnableDLC1(Toggle): + """Shuffle content from The Arctic Cruise (Chapter 6) into the game. This also includes the Tour time rift. + DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" + display_name = "Shuffle Chapter 6" + default = 0 + + +class Tasksanity(Toggle): + """If enabled, Ship Shape tasks will become checks. Requires DLC1 content to be enabled.""" + display_name = "Tasksanity" + default = 0 + + +class TasksanityTaskStep(Range): + """How many tasks the player must complete in Tasksanity to send a check.""" + display_name = "Tasksanity Task Step" + range_start = 1 + range_end = 3 + default = 1 + + +class TasksanityCheckCount(Range): + """How many Tasksanity checks there will be in total.""" + display_name = "Tasksanity Check Count" + range_start = 5 + range_end = 30 + default = 18 + + +class ExcludeTour(Toggle): + """Removes the Tour time rift from the game. This option is recommended if you don't want to deal with + important levels being shuffled onto the Tour time rift, or important items being shuffled onto Tour pages + when your goal is Time's End.""" + display_name = "Exclude Tour Time Rift" + default = 0 + + +class ShipShapeCustomTaskGoal(Range): + """Change the amount of tasks required to complete Ship Shape. This will not affect Cruisin' for a Bruisin'.""" + display_name = "Ship Shape Custom Task Goal" + range_start = 1 + range_end = 30 + default = 18 + + +class EnableDLC2(Toggle): + """Shuffle content from Nyakuza Metro (Chapter 7) into the game. + DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE NYAKUZA METRO DLC INSTALLED!!!""" + display_name = "Shuffle Chapter 7" + default = 0 + + +class MetroMinPonCost(Range): + """The cheapest an item can be in any Nyakuza Metro shop. Includes ticket booths.""" + display_name = "Metro Shops Minimum Pon Cost" + range_start = 10 + range_end = 800 + default = 50 + + +class MetroMaxPonCost(Range): + """The most expensive an item can be in any Nyakuza Metro shop. Includes ticket booths.""" + display_name = "Metro Shops Maximum Pon Cost" + range_start = 10 + range_end = 800 + default = 200 + + +class NyakuzaThugMinShopItems(Range): + """The smallest amount of items that the thugs in Nyakuza Metro can have for sale.""" + display_name = "Nyakuza Thug Minimum Shop Items" + range_start = 0 + range_end = 5 + default = 2 + + +class NyakuzaThugMaxShopItems(Range): + """The largest amount of items that the thugs in Nyakuza Metro can have for sale.""" + display_name = "Nyakuza Thug Maximum Shop Items" + range_start = 0 + range_end = 5 + default = 4 + + +class BaseballBat(Toggle): + """Replace the Umbrella with the baseball bat from Nyakuza Metro. + DLC2 content does not have to be shuffled for this option but Nyakuza Metro still needs to be installed.""" + display_name = "Baseball Bat" + default = 0 + + +class EnableDeathWish(Toggle): + """Shuffle Death Wish contracts into the game. Each contract by default will have 1 check granted upon completion. + DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE SEAL THE DEAL DLC INSTALLED!!!""" + display_name = "Enable Death Wish" + default = 0 + + +class DeathWishOnly(Toggle): + """An alternative gameplay mode that allows you to exclusively play Death Wish in a seed. + This has the following effects: + - Death Wish is instantly unlocked from the start + - All hats and other progression items are instantly given to you + - Useful items such as Fast Hatter Badge will still be in the item pool instead of in your inventory at the start + - All chapters and their levels are unlocked, act shuffle is forced off + - Any checks other than Death Wish contracts are completely removed + - All Pons in the item pool are replaced with Health Pons or random cosmetics + - The EndGoal option is forced to complete Seal the Deal""" + display_name = "Death Wish Only" + default = 0 + + +class DWShuffle(Toggle): + """An alternative mode for Death Wish where each contract is unlocked one by one, in a random order. + Stamp requirements to unlock contracts is removed. Any excluded contracts will not be shuffled into the sequence. + If Seal the Deal is the end goal, it will always be the last Death Wish in the sequence. + Disabling candles is highly recommended.""" + display_name = "Death Wish Shuffle" + default = 0 + + +class DWShuffleCountMin(Range): + """The minimum number of Death Wishes that can be in the Death Wish shuffle sequence. + The final result is clamped at the number of non-excluded Death Wishes.""" + display_name = "Death Wish Shuffle Minimum Count" + range_start = 5 + range_end = 38 + default = 18 + + +class DWShuffleCountMax(Range): + """The maximum number of Death Wishes that can be in the Death Wish shuffle sequence. + The final result is clamped at the number of non-excluded Death Wishes.""" + display_name = "Death Wish Shuffle Maximum Count" + range_start = 5 + range_end = 38 + default = 25 + + +class DWEnableBonus(Toggle): + """In Death Wish, allow the full completion of contracts to reward items. + WARNING!! Only for the brave! This option can create VERY DIFFICULT SEEDS! + ONLY turn this on if you know what you are doing to yourself and everyone else in the multiworld! + Using Peace and Tranquility to auto-complete the bonuses will NOT count!""" + display_name = "Shuffle Death Wish Full Completions" + default = 0 + + +class DWAutoCompleteBonuses(Toggle): + """If enabled, auto complete all bonus stamps after completing the main objective in a Death Wish. + This option will have no effect if bonus checks (DWEnableBonus) are turned on.""" + display_name = "Auto Complete Bonus Stamps" + default = 1 + + +class DWExcludeAnnoyingContracts(Toggle): + """Exclude Death Wish contracts from the pool that are particularly tedious or take a long time to reach/clear. + Excluded Death Wishes are automatically completed as soon as they are unlocked. + This option currently excludes the following contracts: + - Vault Codes in the Wind + - Boss Rush + - Camera Tourist + - The Mustache Gauntlet + - Rift Collapse: Deep Sea + - Cruisin' for a Bruisin' + - Seal the Deal (non-excluded if goal, but the checks are still excluded)""" + display_name = "Exclude Annoying Death Wish Contracts" + default = 1 + + +class DWExcludeAnnoyingBonuses(Toggle): + """If Death Wish full completions are shuffled in, exclude tedious Death Wish full completions from the pool. + Excluded bonus Death Wishes automatically reward their bonus stamps upon completion of the main objective. + This option currently excludes the following bonuses: + - So You're Back From Outer Space + - Encore! Encore! + - Snatcher's Hit List + - 10 Seconds until Self-Destruct + - Killing Two Birds + - Zero Jumps + - Bird Sanctuary + - Wound-Up Windmill + - Seal the Deal""" + display_name = "Exclude Annoying Death Wish Full Completions" + default = 1 + + +class DWExcludeCandles(Toggle): + """If enabled, exclude all candle Death Wishes.""" + display_name = "Exclude Candle Death Wishes" + default = 1 + + +class DWTimePieceRequirement(Range): + """How many Time Pieces that will be required to unlock Death Wish.""" + display_name = "Death Wish Time Piece Requirement" + range_start = 0 + range_end = 35 + default = 15 + + +class TrapChance(Range): + """The chance for any junk item in the pool to be replaced by a trap.""" + display_name = "Trap Chance" + range_start = 0 + range_end = 100 + default = 0 + + +class BabyTrapWeight(Range): + """The weight of Baby Traps in the trap pool. + Baby Traps place a multitude of the Conductor's grandkids into Hat Kid's hands, causing her to lose her balance.""" + display_name = "Baby Trap Weight" + range_start = 0 + range_end = 100 + default = 40 + + +class LaserTrapWeight(Range): + """The weight of Laser Traps in the trap pool. + Laser Traps will spawn multiple giant lasers (from Snatcher's boss fight) at Hat Kid's location.""" + display_name = "Laser Trap Weight" + range_start = 0 + range_end = 100 + default = 40 + + +class ParadeTrapWeight(Range): + """The weight of Parade Traps in the trap pool. + Parade Traps will summon multiple Express Band owls with knives that chase Hat Kid by mimicking her movement.""" + display_name = "Parade Trap Weight" + range_start = 0 + range_end = 100 + default = 20 + + +ahit_options: typing.Dict[str, type(Option)] = { + + "EndGoal": EndGoal, + "ActRandomizer": ActRandomizer, + "ActPlando": ActPlando, + "ShuffleAlpineZiplines": ShuffleAlpineZiplines, + "FinaleShuffle": FinaleShuffle, + "LogicDifficulty": LogicDifficulty, + "YarnBalancePercent": YarnBalancePercent, + "TimePieceBalancePercent": TimePieceBalancePercent, + "RandomizeHatOrder": RandomizeHatOrder, + "UmbrellaLogic": UmbrellaLogic, + "StartWithCompassBadge": StartWithCompassBadge, + "CompassBadgeMode": CompassBadgeMode, + "ShuffleStorybookPages": ShuffleStorybookPages, + "ShuffleActContracts": ShuffleActContracts, + "ShuffleSubconPaintings": ShuffleSubconPaintings, + "NoPaintingSkips": NoPaintingSkips, + "StartingChapter": StartingChapter, + "CTRLogic": CTRLogic, + + "EnableDLC1": EnableDLC1, + "Tasksanity": Tasksanity, + "TasksanityTaskStep": TasksanityTaskStep, + "TasksanityCheckCount": TasksanityCheckCount, + "ExcludeTour": ExcludeTour, + "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, + + "EnableDeathWish": EnableDeathWish, + "DWShuffle": DWShuffle, + "DWShuffleCountMin": DWShuffleCountMin, + "DWShuffleCountMax": DWShuffleCountMax, + "DeathWishOnly": DeathWishOnly, + "DWEnableBonus": DWEnableBonus, + "DWAutoCompleteBonuses": DWAutoCompleteBonuses, + "DWExcludeAnnoyingContracts": DWExcludeAnnoyingContracts, + "DWExcludeAnnoyingBonuses": DWExcludeAnnoyingBonuses, + "DWExcludeCandles": DWExcludeCandles, + "DWTimePieceRequirement": DWTimePieceRequirement, + + "EnableDLC2": EnableDLC2, + "BaseballBat": BaseballBat, + "MetroMinPonCost": MetroMinPonCost, + "MetroMaxPonCost": MetroMaxPonCost, + "NyakuzaThugMinShopItems": NyakuzaThugMinShopItems, + "NyakuzaThugMaxShopItems": NyakuzaThugMaxShopItems, + + "LowestChapterCost": LowestChapterCost, + "HighestChapterCost": HighestChapterCost, + "ChapterCostIncrement": ChapterCostIncrement, + "ChapterCostMinDifference": ChapterCostMinDifference, + "MaxExtraTimePieces": MaxExtraTimePieces, + + "FinalChapterMinCost": FinalChapterMinCost, + "FinalChapterMaxCost": FinalChapterMaxCost, + + "YarnCostMin": YarnCostMin, + "YarnCostMax": YarnCostMax, + "YarnAvailable": YarnAvailable, + "MinExtraYarn": MinExtraYarn, + "HatItems": HatItems, + + "MinPonCost": MinPonCost, + "MaxPonCost": MaxPonCost, + "BadgeSellerMinItems": BadgeSellerMinItems, + "BadgeSellerMaxItems": BadgeSellerMaxItems, + + "TrapChance": TrapChance, + "BabyTrapWeight": BabyTrapWeight, + "LaserTrapWeight": LaserTrapWeight, + "ParadeTrapWeight": ParadeTrapWeight, + + "death_link": DeathLink, +} + +slot_data_options: typing.Dict[str, type(Option)] = { + + "EndGoal": EndGoal, + "ActRandomizer": ActRandomizer, + "ShuffleAlpineZiplines": ShuffleAlpineZiplines, + "LogicDifficulty": LogicDifficulty, + "CTRLogic": CTRLogic, + "RandomizeHatOrder": RandomizeHatOrder, + "UmbrellaLogic": UmbrellaLogic, + "StartWithCompassBadge": StartWithCompassBadge, + "CompassBadgeMode": CompassBadgeMode, + "ShuffleStorybookPages": ShuffleStorybookPages, + "ShuffleActContracts": ShuffleActContracts, + "ShuffleSubconPaintings": ShuffleSubconPaintings, + "NoPaintingSkips": NoPaintingSkips, + "HatItems": HatItems, + + "EnableDLC1": EnableDLC1, + "Tasksanity": Tasksanity, + "TasksanityTaskStep": TasksanityTaskStep, + "TasksanityCheckCount": TasksanityCheckCount, + "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, + "ExcludeTour": ExcludeTour, + + "EnableDeathWish": EnableDeathWish, + "DWShuffle": DWShuffle, + "DeathWishOnly": DeathWishOnly, + "DWEnableBonus": DWEnableBonus, + "DWAutoCompleteBonuses": DWAutoCompleteBonuses, + "DWTimePieceRequirement": DWTimePieceRequirement, + + "EnableDLC2": EnableDLC2, + "MetroMinPonCost": MetroMinPonCost, + "MetroMaxPonCost": MetroMaxPonCost, + "BaseballBat": BaseballBat, + + "MinPonCost": MinPonCost, + "MaxPonCost": MaxPonCost, + + "death_link": DeathLink, +} diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py new file mode 100644 index 0000000000..807f1ee77f --- /dev/null +++ b/worlds/ahit/Regions.py @@ -0,0 +1,900 @@ +from worlds.AutoWorld import World +from BaseClasses import Region, Entrance, ItemClassification, Location +from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem +from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \ + shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard +import typing +from .Rules import set_rift_rules, get_difficulty + + +# ChapterIndex: region +chapter_regions = { + ChapterIndex.SPACESHIP: "Spaceship", + ChapterIndex.MAFIA: "Mafia Town", + ChapterIndex.BIRDS: "Battle of the Birds", + ChapterIndex.SUBCON: "Subcon Forest", + ChapterIndex.ALPINE: "Alpine Skyline", + ChapterIndex.FINALE: "Time's End", + ChapterIndex.CRUISE: "The Arctic Cruise", + ChapterIndex.METRO: "Nyakuza Metro", +} + +# entrance: region +act_entrances = { + "Welcome to Mafia Town": "Mafia Town - Act 1", + "Barrel Battle": "Mafia Town - Act 2", + "She Came from Outer Space": "Mafia Town - Act 3", + "Down with the Mafia!": "Mafia Town - Act 4", + "Cheating the Race": "Mafia Town - Act 5", + "Heating Up Mafia Town": "Mafia Town - Act 6", + "The Golden Vault": "Mafia Town - Act 7", + + "Dead Bird Studio": "Battle of the Birds - Act 1", + "Murder on the Owl Express": "Battle of the Birds - Act 2", + "Picture Perfect": "Battle of the Birds - Act 3", + "Train Rush": "Battle of the Birds - Act 4", + "The Big Parade": "Battle of the Birds - Act 5", + "Award Ceremony": "Battle of the Birds - Finale A", + "Dead Bird Studio Basement": "Battle of the Birds - Finale B", + + "Contractual Obligations": "Subcon Forest - Act 1", + "The Subcon Well": "Subcon Forest - Act 2", + "Toilet of Doom": "Subcon Forest - Act 3", + "Queen Vanessa's Manor": "Subcon Forest - Act 4", + "Mail Delivery Service": "Subcon Forest - Act 5", + "Your Contract has Expired": "Subcon Forest - Finale", + + "Alpine Free Roam": "Alpine Skyline - Free Roam", + "The Illness has Spread": "Alpine Skyline - Finale", + + "The Finale": "Time's End - Act 1", + + "Bon Voyage!": "The Arctic Cruise - Act 1", + "Ship Shape": "The Arctic Cruise - Act 2", + "Rock the Boat": "The Arctic Cruise - Finale", + + "Nyakuza Free Roam": "Nyakuza Metro - Free Roam", + "Rush Hour": "Nyakuza Metro - Finale", +} + +act_chapters = { + "Time Rift - Gallery": "Spaceship", + "Time Rift - The Lab": "Spaceship", + + "Welcome to Mafia Town": "Mafia Town", + "Barrel Battle": "Mafia Town", + "She Came from Outer Space": "Mafia Town", + "Down with the Mafia!": "Mafia Town", + "Cheating the Race": "Mafia Town", + "Heating Up Mafia Town": "Mafia Town", + "The Golden Vault": "Mafia Town", + "Time Rift - Mafia of Cooks": "Mafia Town", + "Time Rift - Sewers": "Mafia Town", + "Time Rift - Bazaar": "Mafia Town", + + "Dead Bird Studio": "Battle of the Birds", + "Murder on the Owl Express": "Battle of the Birds", + "Picture Perfect": "Battle of the Birds", + "Train Rush": "Battle of the Birds", + "The Big Parade": "Battle of the Birds", + "Award Ceremony": "Battle of the Birds", + "Dead Bird Studio Basement": "Battle of the Birds", + "Time Rift - Dead Bird Studio": "Battle of the Birds", + "Time Rift - The Owl Express": "Battle of the Birds", + "Time Rift - The Moon": "Battle of the Birds", + + "Contractual Obligations": "Subcon Forest", + "The Subcon Well": "Subcon Forest", + "Toilet of Doom": "Subcon Forest", + "Queen Vanessa's Manor": "Subcon Forest", + "Mail Delivery Service": "Subcon Forest", + "Your Contract has Expired": "Subcon Forest", + "Time Rift - Sleepy Subcon": "Subcon Forest", + "Time Rift - Pipe": "Subcon Forest", + "Time Rift - Village": "Subcon Forest", + + "Alpine Free Roam": "Alpine Skyline", + "The Illness has Spread": "Alpine Skyline", + "Time Rift - Alpine Skyline": "Alpine Skyline", + "Time Rift - The Twilight Bell": "Alpine Skyline", + "Time Rift - Curly Tail Trail": "Alpine Skyline", + + "The Finale": "Time's End", + "Time Rift - Tour": "Time's End", + + "Bon Voyage!": "The Arctic Cruise", + "Ship Shape": "The Arctic Cruise", + "Rock the Boat": "The Arctic Cruise", + "Time Rift - Balcony": "The Arctic Cruise", + "Time Rift - Deep Sea": "The Arctic Cruise", + + "Nyakuza Free Roam": "Nyakuza Metro", + "Rush Hour": "Nyakuza Metro", + "Time Rift - Rumbi Factory": "Nyakuza Metro", +} + +# region: list[Region] +rift_access_regions = { + "Time Rift - Gallery": ["Spaceship"], + "Time Rift - The Lab": ["Spaceship"], + + "Time Rift - Sewers": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", + "Down with the Mafia!", "Cheating the Race", "Heating Up Mafia Town", + "The Golden Vault"], + + "Time Rift - Bazaar": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", + "Down with the Mafia!", "Cheating the Race", "Heating Up Mafia Town", + "The Golden Vault"], + + "Time Rift - Mafia of Cooks": ["Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", + "Down with the Mafia!", "Cheating the Race", "The Golden Vault"], + + "Time Rift - The Owl Express": ["Murder on the Owl Express"], + "Time Rift - The Moon": ["Picture Perfect", "The Big Parade"], + "Time Rift - Dead Bird Studio": ["Dead Bird Studio", "Dead Bird Studio Basement"], + + "Time Rift - Pipe": ["Contractual Obligations", "The Subcon Well", + "Toilet of Doom", "Queen Vanessa's Manor", + "Mail Delivery Service"], + + "Time Rift - Village": ["Contractual Obligations", "The Subcon Well", + "Toilet of Doom", "Queen Vanessa's Manor", + "Mail Delivery Service"], + + "Time Rift - Sleepy Subcon": ["Contractual Obligations", "The Subcon Well", + "Toilet of Doom", "Queen Vanessa's Manor", + "Mail Delivery Service"], + + "Time Rift - The Twilight Bell": ["Alpine Free Roam"], + "Time Rift - Curly Tail Trail": ["Alpine Free Roam"], + "Time Rift - Alpine Skyline": ["Alpine Free Roam", "The Illness has Spread"], + + "Time Rift - Tour": ["Time's End"], + + "Time Rift - Balcony": ["Cruise Ship"], + "Time Rift - Deep Sea": ["Bon Voyage!"], + + "Time Rift - Rumbi Factory": ["Nyakuza Free Roam"], +} + +# Time piece identifiers to be used in act shuffle +chapter_act_info = { + "Time Rift - Gallery": "Spaceship_WaterRift_Gallery", + "Time Rift - The Lab": "Spaceship_WaterRift_MailRoom", + + "Welcome to Mafia Town": "chapter1_tutorial", + "Barrel Battle": "chapter1_barrelboss", + "She Came from Outer Space": "chapter1_cannon_repair", + "Down with the Mafia!": "chapter1_boss", + "Cheating the Race": "harbor_impossible_race", + "Heating Up Mafia Town": "mafiatown_lava", + "The Golden Vault": "mafiatown_goldenvault", + "Time Rift - Mafia of Cooks": "TimeRift_Cave_Mafia", + "Time Rift - Sewers": "TimeRift_Water_Mafia_Easy", + "Time Rift - Bazaar": "TimeRift_Water_Mafia_Hard", + + "Dead Bird Studio": "DeadBirdStudio", + "Murder on the Owl Express": "chapter3_murder", + "Picture Perfect": "moon_camerasnap", + "Train Rush": "trainwreck_selfdestruct", + "The Big Parade": "moon_parade", + "Award Ceremony": "award_ceremony", + "Dead Bird Studio Basement": "chapter3_secret_finale", + "Time Rift - Dead Bird Studio": "TimeRift_Cave_BirdBasement", + "Time Rift - The Owl Express": "TimeRift_Water_TWreck_Panels", + "Time Rift - The Moon": "TimeRift_Water_TWreck_Parade", + + "Contractual Obligations": "subcon_village_icewall", + "The Subcon Well": "subcon_cave", + "Toilet of Doom": "chapter2_toiletboss", + "Queen Vanessa's Manor": "vanessa_manor_attic", + "Mail Delivery Service": "subcon_maildelivery", + "Your Contract has Expired": "snatcher_boss", + "Time Rift - Sleepy Subcon": "TimeRift_Cave_Raccoon", + "Time Rift - Pipe": "TimeRift_Water_Subcon_Hookshot", + "Time Rift - Village": "TimeRift_Water_Subcon_Dwellers", + + "Alpine Free Roam": "AlpineFreeRoam", # not an actual Time Piece + "The Illness has Spread": "AlpineSkyline_Finale", + "Time Rift - Alpine Skyline": "TimeRift_Cave_Alps", + "Time Rift - The Twilight Bell": "TimeRift_Water_Alp_Goats", + "Time Rift - Curly Tail Trail": "TimeRift_Water_AlpineSkyline_Cats", + + "The Finale": "TheFinale_FinalBoss", + "Time Rift - Tour": "TimeRift_Cave_Tour", + + "Bon Voyage!": "Cruise_Boarding", + "Ship Shape": "Cruise_Working", + "Rock the Boat": "Cruise_Sinking", + "Time Rift - Balcony": "Cruise_WaterRift_Slide", + "Time Rift - Deep Sea": "Cruise_CaveRift_Aquarium", + + "Nyakuza Free Roam": "MetroFreeRoam", # not an actual Time Piece + "Rush Hour": "Metro_Escape", + "Time Rift - Rumbi Factory": "Metro_CaveRift_RumbiFactory" +} + +# Guarantee that the first level a player can access is a location dense area beatable with no items +guaranteed_first_acts = [ + "Welcome to Mafia Town", + "Barrel Battle", + "She Came from Outer Space", + "Down with the Mafia!", + "Heating Up Mafia Town", # Removed in umbrella logic + "The Golden Vault", + + "Contractual Obligations", # Removed in painting logic + "Queen Vanessa's Manor", # Removed in umbrella/painting logic +] + +purple_time_rifts = [ + "Time Rift - Mafia of Cooks", + "Time Rift - Dead Bird Studio", + "Time Rift - Sleepy Subcon", + "Time Rift - Alpine Skyline", + "Time Rift - Deep Sea", + "Time Rift - Tour", + "Time Rift - Rumbi Factory", +] + +chapter_finales = [ + "Dead Bird Studio Basement", + "Your Contract has Expired", + "The Illness has Spread", + "Rock the Boat", + "Rush Hour", +] + +# Acts blacklisted in act shuffle +# entrance: region +blacklisted_acts = { + "Battle of the Birds - Finale A": "Award Ceremony", +} + +# Blacklisted act shuffle combinations to help prevent impossible layouts. Mostly for free roam acts. +blacklisted_combos = { + "The Illness has Spread": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"], + "Rush Hour": ["Nyakuza Free Roam", "Alpine Free Roam", "Contractual Obligations"], + "Time Rift - The Owl Express": ["Alpine Free Roam", "Nyakuza Free Roam", "Bon Voyage!", + "Contractual Obligations"], + + "Time Rift - The Moon": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Dead Bird Studio": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Curly Tail Trail": ["Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - The Twilight Bell": ["Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Alpine Skyline": ["Nyakuza Free Roam", "Contractual Obligations"], + "Time Rift - Rumbi Factory": ["Alpine Free Roam", "Contractual Obligations"], + "Time Rift - Deep Sea": ["Alpine Free Roam", "Nyakuza Free Roam", "Contractual Obligations"], +} + + +def create_regions(world: World): + w = world + mw = world.multiworld + p = world.player + + # ------------------------------------------- HUB -------------------------------------------------- # + menu = create_region(w, "Menu") + spaceship = create_region_and_connect(w, "Spaceship", "Save File -> Spaceship", menu) + + # we only need the menu and the spaceship regions + if world.is_dw_only(): + return + + create_rift_connections(w, create_region(w, "Time Rift - Gallery")) + create_rift_connections(w, create_region(w, "Time Rift - The Lab")) + + # ------------------------------------------- MAFIA TOWN ------------------------------------------- # + mafia_town = create_region_and_connect(w, "Mafia Town", "Telescope -> Mafia Town", spaceship) + mt_act1 = create_region_and_connect(w, "Welcome to Mafia Town", "Mafia Town - Act 1", mafia_town) + mt_act2 = create_region_and_connect(w, "Barrel Battle", "Mafia Town - Act 2", mafia_town) + mt_act3 = create_region_and_connect(w, "She Came from Outer Space", "Mafia Town - Act 3", mafia_town) + mt_act4 = create_region_and_connect(w, "Down with the Mafia!", "Mafia Town - Act 4", mafia_town) + mt_act6 = create_region_and_connect(w, "Heating Up Mafia Town", "Mafia Town - Act 6", mafia_town) + mt_act5 = create_region_and_connect(w, "Cheating the Race", "Mafia Town - Act 5", mafia_town) + mt_act7 = create_region_and_connect(w, "The Golden Vault", "Mafia Town - Act 7", mafia_town) + + # ------------------------------------------- BOTB ------------------------------------------------- # + botb = create_region_and_connect(w, "Battle of the Birds", "Telescope -> Battle of the Birds", spaceship) + dbs = create_region_and_connect(w, "Dead Bird Studio", "Battle of the Birds - Act 1", botb) + create_region_and_connect(w, "Murder on the Owl Express", "Battle of the Birds - Act 2", botb) + pp = create_region_and_connect(w, "Picture Perfect", "Battle of the Birds - Act 3", botb) + tr = create_region_and_connect(w, "Train Rush", "Battle of the Birds - Act 4", botb) + create_region_and_connect(w, "The Big Parade", "Battle of the Birds - Act 5", botb) + create_region_and_connect(w, "Award Ceremony", "Battle of the Birds - Finale A", botb) + basement = create_region_and_connect(w, "Dead Bird Studio Basement", "Battle of the Birds - Finale B", botb) + create_rift_connections(w, create_region(w, "Time Rift - Dead Bird Studio")) + create_rift_connections(w, create_region(w, "Time Rift - The Owl Express")) + create_rift_connections(w, create_region(w, "Time Rift - The Moon")) + + # Items near the Dead Bird Studio elevator can be reached from the basement act, and beyond in Expert + ev_area = create_region_and_connect(w, "Dead Bird Studio - Elevator Area", "DBS -> Elevator Area", dbs) + post_ev_area = create_region_and_connect(w, "Dead Bird Studio - Post Elevator Area", "DBS -> Post Elevator Area", dbs) + connect_regions(basement, ev_area, "DBS Basement -> Elevator Area", p) + if world.multiworld.LogicDifficulty[world.player].value >= int(Difficulty.EXPERT): + connect_regions(basement, post_ev_area, "DBS Basement -> Post Elevator Area", p) + + # ------------------------------------------- SUBCON FOREST --------------------------------------- # + subcon_forest = create_region_and_connect(w, "Subcon Forest", "Telescope -> Subcon Forest", spaceship) + sf_act1 = create_region_and_connect(w, "Contractual Obligations", "Subcon Forest - Act 1", subcon_forest) + sf_act2 = create_region_and_connect(w, "The Subcon Well", "Subcon Forest - Act 2", subcon_forest) + sf_act3 = create_region_and_connect(w, "Toilet of Doom", "Subcon Forest - Act 3", subcon_forest) + sf_act4 = create_region_and_connect(w, "Queen Vanessa's Manor", "Subcon Forest - Act 4", subcon_forest) + sf_act5 = create_region_and_connect(w, "Mail Delivery Service", "Subcon Forest - Act 5", subcon_forest) + create_region_and_connect(w, "Your Contract has Expired", "Subcon Forest - Finale", subcon_forest) + + # ------------------------------------------- ALPINE SKYLINE ------------------------------------------ # + alpine_skyline = create_region_and_connect(w, "Alpine Skyline", "Telescope -> Alpine Skyline", spaceship) + alpine_freeroam = create_region_and_connect(w, "Alpine Free Roam", "Alpine Skyline - Free Roam", alpine_skyline) + alpine_area = create_region_and_connect(w, "Alpine Skyline Area", "AFR -> Alpine Skyline Area", alpine_freeroam) + + # Needs to be separate because there are a lot of locations in Alpine that can't be accessed from Illness + alpine_area_tihs = create_region_and_connect(w, "Alpine Skyline Area (TIHS)", "-> Alpine Skyline Area (TIHS)", + alpine_area) + + create_region_and_connect(w, "The Birdhouse", "-> The Birdhouse", alpine_area) + create_region_and_connect(w, "The Lava Cake", "-> The Lava Cake", alpine_area) + create_region_and_connect(w, "The Windmill", "-> The Windmill", alpine_area) + create_region_and_connect(w, "The Twilight Bell", "-> The Twilight Bell", alpine_area) + + illness = create_region_and_connect(w, "The Illness has Spread", "Alpine Skyline - Finale", alpine_skyline) + connect_regions(illness, alpine_area_tihs, "TIHS -> Alpine Skyline Area (TIHS)", p) + create_rift_connections(w, create_region(w, "Time Rift - Alpine Skyline")) + create_rift_connections(w, create_region(w, "Time Rift - The Twilight Bell")) + create_rift_connections(w, create_region(w, "Time Rift - Curly Tail Trail")) + + # ------------------------------------------- OTHER -------------------------------------------------- # + mt_area: Region = create_region(w, "Mafia Town Area") + mt_area_humt: Region = create_region(w, "Mafia Town Area (HUMT)") + connect_regions(mt_area, mt_area_humt, "MT Area -> MT Area (HUMT)", p) + connect_regions(mt_act1, mt_area, "Mafia Town Entrance WTMT", p) + connect_regions(mt_act2, mt_area, "Mafia Town Entrance BB", p) + connect_regions(mt_act3, mt_area, "Mafia Town Entrance SCFOS", p) + connect_regions(mt_act4, mt_area, "Mafia Town Entrance DWTM", p) + connect_regions(mt_act5, mt_area, "Mafia Town Entrance CTR", p) + connect_regions(mt_act6, mt_area_humt, "Mafia Town Entrance HUMT", p) + connect_regions(mt_act7, mt_area, "Mafia Town Entrance TGV", p) + + create_rift_connections(w, create_region(w, "Time Rift - Mafia of Cooks")) + create_rift_connections(w, create_region(w, "Time Rift - Sewers")) + create_rift_connections(w, create_region(w, "Time Rift - Bazaar")) + + sf_area: Region = create_region(w, "Subcon Forest Area") + connect_regions(sf_act1, sf_area, "Subcon Forest Entrance CO", p) + connect_regions(sf_act2, sf_area, "Subcon Forest Entrance SW", p) + connect_regions(sf_act3, sf_area, "Subcon Forest Entrance TOD", p) + connect_regions(sf_act4, sf_area, "Subcon Forest Entrance QVM", p) + connect_regions(sf_act5, sf_area, "Subcon Forest Entrance MDS", p) + + create_rift_connections(w, create_region(w, "Time Rift - Sleepy Subcon")) + create_rift_connections(w, create_region(w, "Time Rift - Pipe")) + create_rift_connections(w, create_region(w, "Time Rift - Village")) + + badge_seller = create_badge_seller(w) + connect_regions(mt_area, badge_seller, "MT Area -> Badge Seller", p) + connect_regions(mt_area_humt, badge_seller, "MT Area (HUMT) -> Badge Seller", p) + connect_regions(sf_area, badge_seller, "SF Area -> Badge Seller", p) + connect_regions(dbs, badge_seller, "DBS -> Badge Seller", p) + connect_regions(pp, badge_seller, "PP -> Badge Seller", p) + connect_regions(tr, badge_seller, "TR -> Badge Seller", p) + connect_regions(alpine_area_tihs, badge_seller, "ASA -> Badge Seller", p) + + times_end = create_region_and_connect(w, "Time's End", "Telescope -> Time's End", spaceship) + create_region_and_connect(w, "The Finale", "Time's End - Act 1", times_end) + + # ------------------------------------------- DLC1 ------------------------------------------------- # + if w.is_dlc1(): + arctic_cruise = create_region_and_connect(w, "The Arctic Cruise", "Telescope -> The Arctic Cruise", spaceship) + cruise_ship = create_region(w, "Cruise Ship") + + ac_act1 = create_region_and_connect(w, "Bon Voyage!", "The Arctic Cruise - Act 1", arctic_cruise) + ac_act2 = create_region_and_connect(w, "Ship Shape", "The Arctic Cruise - Act 2", arctic_cruise) + ac_act3 = create_region_and_connect(w, "Rock the Boat", "The Arctic Cruise - Finale", arctic_cruise) + + connect_regions(ac_act1, cruise_ship, "Cruise Ship Entrance BV", p) + connect_regions(ac_act2, cruise_ship, "Cruise Ship Entrance SS", p) + connect_regions(ac_act3, cruise_ship, "Cruise Ship Entrance RTB", p) + create_rift_connections(w, create_region(w, "Time Rift - Balcony")) + create_rift_connections(w, create_region(w, "Time Rift - Deep Sea")) + + if mw.ExcludeTour[world.player].value == 0: + create_rift_connections(w, create_region(w, "Time Rift - Tour")) + + if mw.Tasksanity[p].value > 0: + create_tasksanity_locations(w) + + connect_regions(cruise_ship, badge_seller, "CS -> Badge Seller", p) + + if w.is_dlc2(): + nyakuza_metro = create_region_and_connect(w, "Nyakuza Metro", "Telescope -> Nyakuza Metro", spaceship) + metro_freeroam = create_region_and_connect(w, "Nyakuza Free Roam", "Nyakuza Metro - Free Roam", nyakuza_metro) + create_region_and_connect(w, "Rush Hour", "Nyakuza Metro - Finale", nyakuza_metro) + + yellow = create_region_and_connect(w, "Yellow Overpass Station", "-> Yellow Overpass Station", metro_freeroam) + green = create_region_and_connect(w, "Green Clean Station", "-> Green Clean Station", metro_freeroam) + pink = create_region_and_connect(w, "Pink Paw Station", "-> Pink Paw Station", metro_freeroam) + create_region_and_connect(w, "Bluefin Tunnel", "-> Bluefin Tunnel", metro_freeroam) # No manhole + + create_region_and_connect(w, "Yellow Overpass Manhole", "-> Yellow Overpass Manhole", yellow) + create_region_and_connect(w, "Green Clean Manhole", "-> Green Clean Manhole", green) + create_region_and_connect(w, "Pink Paw Manhole", "-> Pink Paw Manhole", pink) + + create_rift_connections(w, create_region(w, "Time Rift - Rumbi Factory")) + create_thug_shops(w) + + +def create_rift_connections(world: World, region: Region): + i = 1 + for name in rift_access_regions[region.name]: + act_region = world.multiworld.get_region(name, world.player) + entrance_name = f"{region.name} Portal - Entrance {i}" + connect_regions(act_region, region, entrance_name, world.player) + i += 1 + + # fix for some weird keyerror from tests + if region.name == "Time Rift - Rumbi Factory": + for entrance in region.entrances: + world.multiworld.get_entrance(entrance.name, world.player) + + +def create_tasksanity_locations(world: World): + ship_shape: Region = world.multiworld.get_region("Ship Shape", world.player) + id_start: int = TASKSANITY_START_ID + for i in range(world.multiworld.TasksanityCheckCount[world.player].value): + location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape) + ship_shape.locations.append(location) + + +def is_valid_plando(world: World, region: str) -> bool: + if region in blacklisted_acts.values(): + return False + + if region not in world.multiworld.ActPlando[world.player].keys(): + return False + + act = world.multiworld.ActPlando[world.player].get(region) + if act in blacklisted_acts.values(): + return False + + # Don't allow plando-ing things onto the first act that aren't completable with nothing + is_first_act: bool = act_chapters[region] == get_first_chapter_region(world).name \ + and region in act_entrances.keys() and ("Act 1" in act_entrances[region] or "Free Roam" in act_entrances[region]) + + if is_first_act: + if act_chapters[act] == "Subcon Forest" and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + return False + + if world.multiworld.UmbrellaLogic[world.player].value > 0 \ + and (act == "Heating Up Mafia Town" or act == "Queen Vanessa's Manor"): + return False + + if act not in guaranteed_first_acts: + return False + + # Don't allow straight up impossible mappings + if region == "The Illness has Spread" and act == "Alpine Free Roam": + return False + + if region == "Rush Hour" and act == "Nyakuza Free Roam": + return False + + if region == "Time Rift - Rumbi Factory" and act == "Nyakuza Free Roam": + return False + + if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": + return False + + return any(a.name == world.multiworld.ActPlando[world.player].get(region) for a in + world.multiworld.get_regions(world.player)) + + +def randomize_act_entrances(world: World): + region_list: typing.List[Region] = get_act_regions(world) + world.random.shuffle(region_list) + + separate_rifts: bool = bool(world.multiworld.ActRandomizer[world.player].value == 1) + + for region in region_list.copy(): + if (act_chapters[region.name] == "Alpine Skyline" or act_chapters[region.name] == "Nyakuza Metro") \ + and "Time Rift" not in region.name: + region_list.remove(region) + region_list.append(region) + + for region in region_list.copy(): + if "Time Rift" in region.name: + region_list.remove(region) + region_list.append(region) + + for region in region_list.copy(): + if region.name in chapter_finales: + region_list.remove(region) + region_list.append(region) + + for region in region_list.copy(): + if region.name in world.multiworld.ActPlando[world.player].keys(): + if is_valid_plando(world, region.name): + region_list.remove(region) + region_list.append(region) + else: + print("Disallowing act plando for", + world.multiworld.player_name[world.player], + "-", region.name, ":", world.multiworld.ActPlando[world.player].get(region.name)) + + # Reverse the list, so we can do what we want to do first + region_list.reverse() + + shuffled_list: typing.List[Region] = [] + mapped_list: typing.List[Region] = [] + rift_dict: typing.Dict[str, Region] = {} + first_chapter: Region = get_first_chapter_region(world) + has_guaranteed: bool = False + + i: int = 0 + while i < len(region_list): + region = region_list[i] + i += 1 + + # Get the first accessible act, so we can map that to something first + if not has_guaranteed: + if act_chapters[region.name] != first_chapter.name: + continue + + if region.name not in act_entrances.keys() or "Act 1" not in act_entrances[region.name] \ + and "Free Roam" not in act_entrances[region.name]: + continue + + if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name): + has_guaranteed = True + + i = 0 + + # Already mapped to something else + if region in mapped_list: + continue + + mapped_list.append(region) + + # Look for candidates to map this act to + candidate_list: typing.List[Region] = [] + for candidate in region_list: + # We're mapping something to the first act, make sure it is valid + if not has_guaranteed: + if candidate.name not in guaranteed_first_acts: + continue + + if candidate.name in world.multiworld.ActPlando[world.player].values(): + continue + + # Not completable without Umbrella + if world.multiworld.UmbrellaLogic[world.player].value > 0 \ + and (candidate.name == "Heating Up Mafia Town" or candidate.name == "Queen Vanessa's Manor"): + continue + + # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either + if world.multiworld.ShuffleSubconPaintings[world.player].value > 0 \ + and "Subcon Forest" in act_entrances[candidate.name]: + continue + + candidate_list.append(candidate) + has_guaranteed = True + break + + if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name): + candidate_list.clear() + candidate_list.append( + world.multiworld.get_region(world.multiworld.ActPlando[world.player].get(region.name), world.player)) + break + + # Already mapped onto something else + if candidate in shuffled_list: + continue + + if separate_rifts: + # Don't map Time Rifts to normal acts + if "Time Rift" in region.name and "Time Rift" not in candidate.name: + continue + + # Don't map normal acts to Time Rifts + if "Time Rift" not in region.name and "Time Rift" in candidate.name: + continue + + # Separate purple rifts + if region.name in purple_time_rifts and candidate.name not in purple_time_rifts \ + or region.name not in purple_time_rifts and candidate.name in purple_time_rifts: + continue + + if region.name in blacklisted_combos.keys() and candidate.name in blacklisted_combos[region.name]: + continue + + # Prevent Contractual Obligations from being inaccessible if contracts are not shuffled + if world.multiworld.ShuffleActContracts[world.player].value == 0: + if (region.name == "Your Contract has Expired" or region.name == "The Subcon Well") \ + and candidate.name == "Contractual Obligations": + continue + + if world.multiworld.FinaleShuffle[world.player].value > 0 and region.name in chapter_finales: + if candidate.name not in chapter_finales: + continue + + if region.name in rift_access_regions and candidate.name in rift_access_regions[region.name]: + continue + + candidate_list.append(candidate) + + candidate: Region + if len(candidate_list) > 0: + candidate = candidate_list[world.random.randint(0, len(candidate_list)-1)] + else: + # plando can still break certain rules, so acts may not always end up shuffled. + for c in region_list: + if c not in shuffled_list: + candidate = c + break + + shuffled_list.append(candidate) + # print(region, candidate) + + # Vanilla + if candidate.name == region.name: + if region.name in rift_access_regions.keys(): + rift_dict.setdefault(region.name, candidate) + + update_chapter_act_info(world, region, candidate) + continue + + if region.name in rift_access_regions.keys(): + connect_time_rift(world, region, candidate) + rift_dict.setdefault(region.name, candidate) + else: + if candidate.name in rift_access_regions.keys(): + for e in candidate.entrances.copy(): + e.parent_region.exits.remove(e) + e.connected_region.entrances.remove(e) + + entrance = world.multiworld.get_entrance(act_entrances[region.name], world.player) + reconnect_regions(entrance, world.multiworld.get_region(act_chapters[region.name], world.player), candidate) + + update_chapter_act_info(world, region, candidate) + + for name in blacklisted_acts.values(): + if not is_act_blacklisted(world, name): + continue + + region: Region = world.multiworld.get_region(name, world.player) + update_chapter_act_info(world, region, region) + + set_rift_rules(world, rift_dict) + + +def connect_time_rift(world: World, time_rift: Region, exit_region: Region): + count: int = len(rift_access_regions[time_rift.name]) + i: int = 1 + while i <= count: + name = f"{time_rift.name} Portal - Entrance {i}" + entrance: Entrance = world.multiworld.get_entrance(name, world.player) + reconnect_regions(entrance, entrance.parent_region, exit_region) + i += 1 + + +def get_act_regions(world: World) -> typing.List[Region]: + act_list: typing.List[Region] = [] + for region in world.multiworld.get_regions(world.player): + if region.name in chapter_act_info.keys(): + if not is_act_blacklisted(world, region.name): + act_list.append(region) + + return act_list + + +def is_act_blacklisted(world: World, name: str) -> bool: + plando: bool = name in world.multiworld.ActPlando[world.player].keys() \ + or name in world.multiworld.ActPlando[world.player].values() + + if name == "The Finale": + return not plando and world.multiworld.EndGoal[world.player].value == 1 + + if name == "Rush Hour": + return not plando and world.multiworld.EndGoal[world.player].value == 2 + + if name == "Time Rift - Tour": + return world.multiworld.ExcludeTour[world.player].value > 0 + + return name in blacklisted_acts.values() + + +def create_region(world: World, name: str) -> Region: + reg = Region(name, world.player, world.multiworld) + + for (key, data) in location_table.items(): + if world.is_dw_only(): + break + + if data.nyakuza_thug != "": + continue + + if data.region == name: + if key in storybook_pages.keys() \ + and world.multiworld.ShuffleStorybookPages[world.player].value == 0: + continue + + location = HatInTimeLocation(world.player, key, data.id, reg) + reg.locations.append(location) + if location.name in shop_locations: + world.shop_locs.append(location.name) + + world.multiworld.regions.append(reg) + return reg + + +def create_badge_seller(world: World) -> Region: + badge_seller = Region("Badge Seller", world.player, world.multiworld) + world.multiworld.regions.append(badge_seller) + count: int = 0 + max_items: int = 0 + + if world.multiworld.BadgeSellerMaxItems[world.player].value > 0: + max_items = world.random.randint(world.multiworld.BadgeSellerMinItems[world.player].value, + world.multiworld.BadgeSellerMaxItems[world.player].value) + + if max_items <= 0: + world.set_badge_seller_count(0) + return badge_seller + + for (key, data) in shop_locations.items(): + if "Badge Seller" not in key: + continue + + location = HatInTimeLocation(world.player, key, data.id, badge_seller) + badge_seller.locations.append(location) + world.shop_locs.append(location.name) + + count += 1 + if count >= max_items: + break + + world.set_badge_seller_count(max_items) + return badge_seller + + +def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: + entrance = Entrance(player, entrancename, start_region) + start_region.exits.append(entrance) + entrance.connect(exit_region) + return entrance + + +# Takes an entrance, removes its old connections, and reconnects it between the two regions specified. +def reconnect_regions(entrance: Entrance, start_region: Region, exit_region: Region): + if entrance in entrance.connected_region.entrances: + entrance.connected_region.entrances.remove(entrance) + + if entrance in entrance.parent_region.exits: + entrance.parent_region.exits.remove(entrance) + + if entrance in start_region.exits: + start_region.exits.remove(entrance) + + if entrance in exit_region.entrances: + exit_region.entrances.remove(entrance) + + entrance.parent_region = start_region + start_region.exits.append(entrance) + entrance.connect(exit_region) + + +def create_region_and_connect(world: World, + name: str, entrancename: str, connected_region: Region, is_exit: bool = True) -> Region: + + reg: Region = create_region(world, name) + entrance_region: Region + exit_region: Region + + if is_exit: + entrance_region = connected_region + exit_region = reg + else: + entrance_region = reg + exit_region = connected_region + + connect_regions(entrance_region, exit_region, entrancename, world.player) + return reg + + +def get_first_chapter_region(world: World) -> Region: + start_chapter: ChapterIndex = world.multiworld.StartingChapter[world.player] + return world.multiworld.get_region(chapter_regions.get(start_chapter), world.player) + + +def get_act_original_chapter(world: World, act_name: str) -> Region: + return world.multiworld.get_region(act_chapters[act_name], world.player) + + +# Sets an act entrance in slot data by specifying the Hat_ChapterActInfo, to be used in-game +def update_chapter_act_info(world: World, original_region: Region, new_region: Region): + original_act_info = chapter_act_info[original_region.name] + new_act_info = chapter_act_info[new_region.name] + world.act_connections[original_act_info] = new_act_info + + +def get_shuffled_region(self, region: str) -> str: + ci: str = chapter_act_info[region] + for key, val in self.act_connections.items(): + if val == ci: + for name in chapter_act_info.keys(): + if chapter_act_info[name] == key: + return name + + +def create_thug_shops(world: World): + min_items: int = world.multiworld.NyakuzaThugMinShopItems[world.player].value + + max_items: int = world.multiworld.NyakuzaThugMaxShopItems[world.player].value + count: int = -1 + step: int = 0 + old_name: str = "" + thug_items = world.get_nyakuza_thug_items() + + for key, data in shop_locations.items(): + if data.nyakuza_thug == "": + continue + + if old_name != "" and old_name == data.nyakuza_thug: + continue + + try: + if thug_items[data.nyakuza_thug] <= 0: + continue + except KeyError: + pass + + if count == -1: + count = world.random.randint(min_items, max_items) + thug_items.setdefault(data.nyakuza_thug, count) + if count <= 0: + continue + + if count >= 1: + region = world.multiworld.get_region(data.region, world.player) + loc = HatInTimeLocation(world.player, key, data.id, region) + region.locations.append(loc) + world.shop_locs.append(loc.name) + + step += 1 + if step >= count: + old_name = data.nyakuza_thug + step = 0 + count = -1 + + world.set_nyakuza_thug_items(thug_items) + + +def create_events(world: World) -> int: + count: int = 0 + + for (name, data) in event_locs.items(): + if not is_location_valid(world, name): + continue + + item_name: str = name + if world.is_dw(): + if name in snatcher_coins.keys(): + name = f"{name} ({data.region})" + elif name in zero_jumps: + if get_difficulty(world) < Difficulty.HARD and name in zero_jumps_hard: + continue + + if get_difficulty(world) < Difficulty.EXPERT and name in zero_jumps_expert: + continue + + event: Location = create_event(name, item_name, world.multiworld.get_region(data.region, world.player), world) + event.show_in_spoiler = False + count += 1 + + return count + + +def create_event(name: str, item_name: str, region: Region, world: World) -> Location: + event = HatInTimeLocation(world.player, name, None, region) + region.locations.append(event) + event.place_locked_item(HatInTimeItem(item_name, ItemClassification.progression, None, world.player)) + return event diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py new file mode 100644 index 0000000000..7eb09bedfc --- /dev/null +++ b/worlds/ahit/Rules.py @@ -0,0 +1,944 @@ +from worlds.AutoWorld import World, CollectionState +from worlds.generic.Rules import add_rule, set_rule +from .Locations import location_table, zipline_unlocks, is_location_valid, contract_locations, \ + shop_locations, event_locs, snatcher_coins +from .Types import HatType, ChapterIndex, hat_type_to_item, Difficulty, HatDLC +from BaseClasses import Location, Entrance, Region +import typing + + +act_connections = { + "Mafia Town - Act 2": ["Mafia Town - Act 1"], + "Mafia Town - Act 3": ["Mafia Town - Act 1"], + "Mafia Town - Act 4": ["Mafia Town - Act 2", "Mafia Town - Act 3"], + "Mafia Town - Act 6": ["Mafia Town - Act 4"], + "Mafia Town - Act 7": ["Mafia Town - Act 4"], + "Mafia Town - Act 5": ["Mafia Town - Act 6", "Mafia Town - Act 7"], + + "Battle of the Birds - Act 2": ["Battle of the Birds - Act 1"], + "Battle of the Birds - Act 3": ["Battle of the Birds - Act 1"], + "Battle of the Birds - Act 4": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"], + "Battle of the Birds - Act 5": ["Battle of the Birds - Act 2", "Battle of the Birds - Act 3"], + "Battle of the Birds - Finale A": ["Battle of the Birds - Act 4", "Battle of the Birds - Act 5"], + "Battle of the Birds - Finale B": ["Battle of the Birds - Finale A"], + + "Subcon Forest - Finale": ["Subcon Forest - Act 1", "Subcon Forest - Act 2", + "Subcon Forest - Act 3", "Subcon Forest - Act 4", + "Subcon Forest - Act 5"], + + "The Arctic Cruise - Act 2": ["The Arctic Cruise - Act 1"], + "The Arctic Cruise - Finale": ["The Arctic Cruise - Act 2"], +} + + +def can_use_hat(state: CollectionState, world: World, hat: HatType) -> bool: + if world.multiworld.HatItems[world.player].value > 0: + return state.has(hat_type_to_item[hat], world.player) + + return state.count("Yarn", world.player) >= get_hat_cost(world, hat) + + +def get_hat_cost(world: World, hat: HatType) -> int: + cost: int = 0 + costs = world.get_hat_yarn_costs() + for h in world.get_hat_craft_order(): + cost += costs[h] + if h == hat: + break + + return cost + + +def can_sdj(state: CollectionState, world: World): + return can_use_hat(state, world, HatType.SPRINT) + + +def painting_logic(world: World) -> bool: + return world.multiworld.ShuffleSubconPaintings[world.player].value > 0 + + +# -1 = Normal, 0 = Moderate, 1 = Hard, 2 = Expert +def get_difficulty(world: World) -> Difficulty: + return Difficulty(world.multiworld.LogicDifficulty[world.player].value) + + +def has_paintings(state: CollectionState, world: World, count: int, allow_skip: bool = True) -> bool: + if not painting_logic(world): + return True + + if world.multiworld.NoPaintingSkips[world.player].value == 0 and allow_skip: + # In Moderate there is a very easy trick to skip all the walls, except for the one guarding the boss arena + if get_difficulty(world) >= Difficulty.MODERATE: + return True + + return state.count("Progressive Painting Unlock", world.player) >= count + + +def zipline_logic(world: World) -> bool: + return world.multiworld.ShuffleAlpineZiplines[world.player].value > 0 + + +def can_use_hookshot(state: CollectionState, world: World): + return state.has("Hookshot Badge", world.player) + + +def can_hit(state: CollectionState, world: World, umbrella_only: bool = False): + if world.multiworld.UmbrellaLogic[world.player].value == 0: + return True + + return state.has("Umbrella", world.player) or not umbrella_only and can_use_hat(state, world, HatType.BREWING) + + +def can_surf(state: CollectionState, world: World): + return state.has("No Bonk Badge", world.player) + + +def has_relic_combo(state: CollectionState, world: World, relic: str) -> bool: + return state.has_group(relic, world.player, len(world.item_name_groups[relic])) + + +def get_relic_count(state: CollectionState, world: World, relic: str) -> int: + return state.count_group(relic, world.player) + + +# Only use for rifts +def can_clear_act(state: CollectionState, world: World, act_entrance: str) -> bool: + entrance: Entrance = world.multiworld.get_entrance(act_entrance, world.player) + if not state.can_reach(entrance.connected_region, "Region", world.player): + return False + + if "Free Roam" in entrance.connected_region.name: + return True + + name: str = f"Act Completion ({entrance.connected_region.name})" + return world.multiworld.get_location(name, world.player).access_rule(state) + + +def can_clear_alpine(state: CollectionState, world: World) -> bool: + return state.has("Birdhouse Cleared", world.player) and state.has("Lava Cake Cleared", world.player) \ + and state.has("Windmill Cleared", world.player) and state.has("Twilight Bell Cleared", world.player) + + +def can_clear_metro(state: CollectionState, world: World) -> bool: + return state.has("Nyakuza Intro Cleared", world.player) \ + and state.has("Yellow Overpass Station Cleared", world.player) \ + and state.has("Yellow Overpass Manhole Cleared", world.player) \ + and state.has("Green Clean Station Cleared", world.player) \ + and state.has("Green Clean Manhole Cleared", world.player) \ + and state.has("Bluefin Tunnel Cleared", world.player) \ + and state.has("Pink Paw Station Cleared", world.player) \ + and state.has("Pink Paw Manhole Cleared", world.player) + + +def set_rules(world: World): + # First, chapter access + starting_chapter = ChapterIndex(world.multiworld.StartingChapter[world.player].value) + world.set_chapter_cost(starting_chapter, 0) + + # Chapter costs increase progressively. Randomly decide the chapter order, except for Finale + chapter_list: typing.List[ChapterIndex] = [ChapterIndex.MAFIA, ChapterIndex.BIRDS, + ChapterIndex.SUBCON, ChapterIndex.ALPINE] + + final_chapter = ChapterIndex.FINALE + if world.multiworld.EndGoal[world.player].value == 2: + final_chapter = ChapterIndex.METRO + chapter_list.append(ChapterIndex.FINALE) + elif world.multiworld.EndGoal[world.player].value == 3: + final_chapter = None + chapter_list.append(ChapterIndex.FINALE) + + if world.is_dlc1(): + chapter_list.append(ChapterIndex.CRUISE) + + if world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + chapter_list.append(ChapterIndex.METRO) + + chapter_list.remove(starting_chapter) + world.random.shuffle(chapter_list) + + if starting_chapter is not ChapterIndex.ALPINE and (world.is_dlc1() or world.is_dlc2()): + index1: int = 69 + index2: int = 69 + pos: int + lowest_index: int + chapter_list.remove(ChapterIndex.ALPINE) + + if world.is_dlc1(): + index1 = chapter_list.index(ChapterIndex.CRUISE) + + if world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + index2 = chapter_list.index(ChapterIndex.METRO) + + lowest_index = min(index1, index2) + if lowest_index == 0: + pos = 0 + else: + pos = world.random.randint(0, lowest_index) + + chapter_list.insert(pos, ChapterIndex.ALPINE) + + if world.is_dlc1() and world.is_dlc2() and final_chapter is not ChapterIndex.METRO: + chapter_list.remove(ChapterIndex.METRO) + index = chapter_list.index(ChapterIndex.CRUISE) + if index >= len(chapter_list): + chapter_list.append(ChapterIndex.METRO) + else: + chapter_list.insert(world.random.randint(index+1, len(chapter_list)), ChapterIndex.METRO) + + lowest_cost: int = world.multiworld.LowestChapterCost[world.player].value + highest_cost: int = world.multiworld.HighestChapterCost[world.player].value + + cost_increment: int = world.multiworld.ChapterCostIncrement[world.player].value + min_difference: int = world.multiworld.ChapterCostMinDifference[world.player].value + last_cost: int = 0 + cost: int + loop_count: int = 0 + + for chapter in chapter_list: + min_range: int = lowest_cost + (cost_increment * loop_count) + if min_range >= highest_cost: + min_range = highest_cost-1 + + value: int = world.random.randint(min_range, min(highest_cost, max(lowest_cost, last_cost + cost_increment))) + + cost = world.random.randint(value, min(value + cost_increment, highest_cost)) + if loop_count >= 1: + if last_cost + min_difference > cost: + cost = last_cost + min_difference + + cost = min(cost, highest_cost) + world.set_chapter_cost(chapter, cost) + last_cost = cost + loop_count += 1 + + if final_chapter is not None: + world.set_chapter_cost(final_chapter, world.random.randint( + world.multiworld.FinalChapterMinCost[world.player].value, + world.multiworld.FinalChapterMaxCost[world.player].value)) + + add_rule(world.multiworld.get_entrance("Telescope -> Mafia Town", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.MAFIA))) + + add_rule(world.multiworld.get_entrance("Telescope -> Battle of the Birds", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + + add_rule(world.multiworld.get_entrance("Telescope -> Subcon Forest", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.SUBCON))) + + add_rule(world.multiworld.get_entrance("Telescope -> Alpine Skyline", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) + + add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE)) + and can_use_hat(state, world, HatType.BREWING) and can_use_hat(state, world, HatType.DWELLER)) + + if world.is_dlc1(): + add_rule(world.multiworld.get_entrance("Telescope -> The Arctic Cruise", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.CRUISE))) + + if world.is_dlc2(): + add_rule(world.multiworld.get_entrance("Telescope -> Nyakuza Metro", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE)) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.METRO)) + and can_use_hat(state, world, HatType.DWELLER) and can_use_hat(state, world, HatType.ICE)) + + if world.multiworld.ActRandomizer[world.player].value == 0: + set_default_rift_rules(world) + + table = location_table | event_locs + location: Location + for (key, data) in table.items(): + if not is_location_valid(world, key): + continue + + if key in contract_locations.keys(): + continue + + if data.dlc_flags & HatDLC.death_wish and key in snatcher_coins.keys(): + key = f"{key} ({data.region})" + + location = world.multiworld.get_location(key, world.player) + + for hat in data.required_hats: + if hat is not HatType.NONE: + add_rule(location, lambda state, h=hat: can_use_hat(state, world, h)) + + if data.hookshot: + add_rule(location, lambda state: can_use_hookshot(state, world)) + + if data.umbrella and world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(location, lambda state: state.has("Umbrella", world.player)) + + if data.paintings > 0 and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + add_rule(location, lambda state, paintings=data.paintings: has_paintings(state, world, paintings)) + + if data.hit_requirement > 0: + if data.hit_requirement == 1: + add_rule(location, lambda state: can_hit(state, world)) + elif data.hit_requirement == 2: # Can bypass with Dweller Mask (dweller bells) + add_rule(location, lambda state: can_hit(state, world) or can_use_hat(state, world, HatType.DWELLER)) + + for misc in data.misc_required: + add_rule(location, lambda state, item=misc: state.has(item, world.player)) + + set_specific_rules(world) + + # Putting all of this here, so it doesn't get overridden by anything + # Illness starts the player past the intro + alpine_entrance = world.multiworld.get_entrance("AFR -> Alpine Skyline Area", world.player) + add_rule(alpine_entrance, lambda state: can_use_hookshot(state, world)) + if world.multiworld.UmbrellaLogic[world.player].value > 0: + add_rule(alpine_entrance, lambda state: state.has("Umbrella", world.player)) + + if zipline_logic(world): + add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), + lambda state: state.has("Zipline Unlock - The Lava Cake Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), + lambda state: state.has("Zipline Unlock - The Windmill Path", world.player)) + + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: state.has("Zipline Unlock - The Twilight Bell Path", world.player)) + + add_rule(world.multiworld.get_location("Act Completion (The Illness has Spread)", world.player), + lambda state: state.has("Zipline Unlock - The Birdhouse Path", world.player) + and state.has("Zipline Unlock - The Lava Cake Path", world.player) + and state.has("Zipline Unlock - The Windmill Path", world.player)) + + if zipline_logic(world): + for (loc, zipline) in zipline_unlocks.items(): + add_rule(world.multiworld.get_location(loc, world.player), + lambda state, z=zipline: state.has(z, world.player)) + + for loc in world.multiworld.get_region("Alpine Skyline Area (TIHS)", world.player).locations: + if "Goat Village" in loc.name: + continue + + add_rule(loc, lambda state: can_use_hookshot(state, world)) + + for (key, acts) in act_connections.items(): + if "Arctic Cruise" in key and not world.is_dlc1(): + continue + + i: int = 1 + entrance: Entrance = world.multiworld.get_entrance(key, world.player) + region: Region = entrance.connected_region + access_rules: typing.List[typing.Callable[[CollectionState], bool]] = [] + entrance.parent_region.exits.remove(entrance) + + # Entrances to this act that we have to set access_rules on + entrances: typing.List[Entrance] = [] + + for act in acts: + act_entrance: Entrance = world.multiworld.get_entrance(act, world.player) + access_rules.append(act_entrance.access_rule) + required_region = act_entrance.connected_region + name: str = f"{key}: Connection {i}" + new_entrance: Entrance = connect_regions(required_region, region, name, world.player) + entrances.append(new_entrance) + + # Copy access rules from act completions + if "Free Roam" not in required_region.name: + rule: typing.Callable[[CollectionState], bool] + name = f"Act Completion ({required_region.name})" + rule = world.multiworld.get_location(name, world.player).access_rule + access_rules.append(rule) + + i += 1 + + for e in entrances: + for rules in access_rules: + add_rule(e, rules) + + set_event_rules(world) + + if world.multiworld.EndGoal[world.player].value == 1: + world.multiworld.completion_condition[world.player] = lambda state: state.has("Time Piece Cluster", world.player) + elif world.multiworld.EndGoal[world.player].value == 2: + world.multiworld.completion_condition[world.player] = lambda state: state.has("Rush Hour Cleared", world.player) + + +def set_specific_rules(world: World): + add_rule(world.multiworld.get_location("Mafia Boss Shop Item", world.player), + lambda state: state.has("Time Piece", world.player, 12) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + + add_rule(world.multiworld.get_location("Spaceship - Rumbi Abuse", world.player), + lambda state: state.has("Time Piece", world.player, 4)) + + set_mafia_town_rules(world) + set_botb_rules(world) + set_subcon_rules(world) + set_alps_rules(world) + + if world.is_dlc1(): + set_dlc1_rules(world) + + if world.is_dlc2(): + set_dlc2_rules(world) + + difficulty: Difficulty = get_difficulty(world) + + if difficulty >= Difficulty.MODERATE: + set_moderate_rules(world) + + if difficulty >= Difficulty.HARD: + set_hard_rules(world) + + if difficulty >= 2: + set_expert_rules(world) + + +def set_moderate_rules(world: World): + # Moderate: Gallery without Brewing Hat + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Gallery)", world.player), lambda state: True) + + # Moderate: Above Boats via Ice Hat Sliding + add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), + lambda state: can_use_hat(state, world, HatType.ICE), "or") + + # Moderate: Clock Tower Chest + Ruined Tower with nothing + add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True) + add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True) + + # Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell + for loc in world.multiworld.get_region("The Subcon Well", world.player).locations: + set_rule(loc, lambda state: has_paintings(state, world, 1)) + + # Moderate: Vanessa Manor with nothing + for loc in world.multiworld.get_region("Queen Vanessa's Manor", world.player).locations: + set_rule(loc, lambda state: True) + + set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), lambda state: True) + + # Moderate: get to Birdhouse/Yellow Band Hills without Brewing Hat + set_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: can_use_hookshot(state, world)) + set_rule(world.multiworld.get_location("Alpine Skyline - Yellow Band Hills", world.player), + lambda state: can_use_hookshot(state, world)) + + # Moderate: The Birdhouse - Dweller Platforms Relic with only Birdhouse access + set_rule(world.multiworld.get_location("Alpine Skyline - The Birdhouse: Dweller Platforms Relic", world.player), + lambda state: True) + + # Moderate: Twilight Path without Dweller Mask + set_rule(world.multiworld.get_location("Alpine Skyline - The Twilight Path", world.player), lambda state: True) + + # Moderate: Mystifying Time Mesa time trial without hats + set_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), + lambda state: can_use_hookshot(state, world)) + + # Moderate: Finale without Hookshot + set_rule(world.multiworld.get_location("Act Completion (The Finale)", world.player), + lambda state: can_use_hat(state, world, HatType.DWELLER)) + + if world.is_dlc1(): + # Moderate: clear Rock the Boat without Ice Hat + add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True) + add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True) + + # Moderate: clear Deep Sea without Ice Hat + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), + lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) + + # There is a glitched fall damage volume near the Yellow Overpass time piece that warps the player to Pink Paw. + # Yellow Overpass time piece can also be reached without Hookshot quite easily. + if world.is_dlc2(): + set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Station)", world.player), + lambda state: True) + + set_rule(world.multiworld.get_location("Pink Paw Station - Cat Vacuum", world.player), lambda state: True) + + # The player can quite literally walk past the fan from the side without Time Stop. + set_rule(world.multiworld.get_location("Pink Paw Station - Behind Fan", world.player), lambda state: True) + + # Moderate: clear Rush Hour without Hookshot + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: state.has("Metro Ticket - Pink", world.player) + and state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and can_use_hat(state, world, HatType.ICE) + and can_use_hat(state, world, HatType.BREWING)) + + # Moderate: Bluefin Tunnel without tickets + set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) + + +def set_hard_rules(world: World): + # Hard: clear Time Rift - The Twilight Bell with Sprint+Scooter only + add_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) + and state.has("Scooter Badge", world.player), "or") + + # No Dweller Mask required + set_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player), + lambda state: has_paintings(state, world, 3)) + + # Cherry bridge over boss arena gap (painting still expected) + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player)) + + # SDJ + add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: can_sdj(state, world) and has_paintings(state, world, 2), "or") + + add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), + lambda state: has_paintings(state, world, 3) and can_sdj(state, world), "or") + + add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), + lambda state: can_sdj(state, world), "or") + + # Finale Telescope with only Ice Hat + add_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: can_use_hat(state, world, HatType.ICE), "or") + + if world.is_dlc1(): + # Hard: clear Deep Sea without Dweller Mask + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player), + lambda state: can_use_hookshot(state, world)) + + if world.is_dlc2(): + # Hard: clear Green Clean Manhole without Dweller Mask + set_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player), + lambda state: can_use_hat(state, world, HatType.ICE)) + + # Hard: clear Rush Hour with Brewing Hat only + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING)) + + +def set_expert_rules(world: World): + # Finale Telescope with no hats + set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), + lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE))) + + # Expert: Mafia Town - Above Boats with nothing + set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True) + + # Expert: Clear Dead Bird Studio with nothing + for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations: + set_rule(loc, lambda state: True) + + set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), lambda state: True) + + # Expert: get to and clear Twilight Bell without Dweller Mask. + # Dweller Mask OR Sprint Hat OR Brewing Hat OR Time Stop + Umbrella required to complete act. + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: can_use_hookshot(state, world), "or") + + add_rule(world.multiworld.get_location("Act Completion (The Twilight Bell)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING) + or can_use_hat(state, world, HatType.DWELLER) + or can_use_hat(state, world, HatType.SPRINT) + or (can_use_hat(state, world, HatType.TIME_STOP) and state.has("Umbrella", world.player))) + + # Expert: Time Rift - Curly Tail Trail with nothing + # Time Rift - Twilight Bell and Time Rift - Village with nothing + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), + lambda state: True) + + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Time Rift - The Twilight Bell)", world.player), + lambda state: True) + + # Expert: Cherry Hovering + entrance = connect_regions(world.multiworld.get_region("Your Contract has Expired", world.player), + world.multiworld.get_region("Subcon Forest Area", world.player), + "Subcon Forest Entrance YCHE", world.player) + + if world.multiworld.NoPaintingSkips[world.player].value > 0: + add_rule(entrance, lambda state: has_paintings(state, world, 1)) + + set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world) + and has_paintings(state, world, 1, True)) + + # Set painting rules only. Skipping paintings is determined in has_paintings + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: has_paintings(state, world, 1, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), + lambda state: has_paintings(state, world, 3, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), + lambda state: has_paintings(state, world, 3, True)) + + # You can cherry hover to Snatcher's post-fight cutscene, which completes the level without having to fight him + connect_regions(world.multiworld.get_region("Subcon Forest Area", world.player), + world.multiworld.get_region("Your Contract has Expired", world.player), + "Snatcher Hover", world.player) + set_rule(world.multiworld.get_location("Act Completion (Your Contract has Expired)", world.player), + lambda state: True) + + if world.is_dlc2(): + # Expert: clear Rush Hour with nothing + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) + + +def set_mafia_town_rules(world: World): + add_rule(world.multiworld.get_location("Mafia Town - Behind HQ Chest", world.player), + lambda state: state.can_reach("Act Completion (Heating Up Mafia Town)", "Location", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player)) + + # Old guys don't appear in SCFOS + add_rule(world.multiworld.get_location("Mafia Town - Old Man (Steel Beams)", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player)) + + add_rule(world.multiworld.get_location("Mafia Town - Old Man (Seaside Spaghetti)", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player)) + + # Only available outside She Came from Outer Space + add_rule(world.multiworld.get_location("Mafia Town - Mafia Geek Platform", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("Down with the Mafia!", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("Heating Up Mafia Town", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player)) + + # Only available outside Down with the Mafia! (for some reason) + add_rule(world.multiworld.get_location("Mafia Town - On Scaffolding", world.player), + lambda state: state.can_reach("Welcome to Mafia Town", "Region", world.player) + or state.can_reach("Barrel Battle", "Region", world.player) + or state.can_reach("She Came from Outer Space", "Region", world.player) + or state.can_reach("Cheating the Race", "Region", world.player) + or state.can_reach("Heating Up Mafia Town", "Region", world.player) + or state.can_reach("The Golden Vault", "Region", world.player)) + + # For some reason, the brewing crate is removed in HUMT + add_rule(world.multiworld.get_location("Mafia Town - Secret Cave", world.player), + lambda state: state.has("HUMT Access", world.player), "or") + + # Can bounce across the lava to get this without Hookshot (need to die though) + add_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), + lambda state: state.has("HUMT Access", world.player), "or") + + ctr_logic: int = world.multiworld.CTRLogic[world.player].value + if ctr_logic == 3: + set_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), lambda state: True) + elif ctr_logic == 2: + add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT), "or") + elif ctr_logic == 1: + add_rule(world.multiworld.get_location("Act Completion (Cheating the Race)", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) + and state.has("Scooter Badge", world.player), "or") + + +def set_botb_rules(world: World): + if world.multiworld.UmbrellaLogic[world.player].value == 0 and get_difficulty(world) < Difficulty.MODERATE: + set_rule(world.multiworld.get_location("Dead Bird Studio - DJ Grooves Sign Chest", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + set_rule(world.multiworld.get_location("Dead Bird Studio - Tepee Chest", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + set_rule(world.multiworld.get_location("Dead Bird Studio - Conductor Chest", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + set_rule(world.multiworld.get_location("Act Completion (Dead Bird Studio)", world.player), + lambda state: state.has("Umbrella", world.player) or can_use_hat(state, world, HatType.BREWING)) + + +def set_subcon_rules(world: World): + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING) or state.has("Umbrella", world.player) + or can_use_hat(state, world, HatType.DWELLER)) + + # You can't skip over the boss arena wall without cherry hover, so these two need to be set this way + set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), + lambda state: state.has("TOD Access", world.player) and can_use_hookshot(state, world) + and has_paintings(state, world, 1, False) or state.has("YCHE Access", world.player)) + + # The painting wall can't be skipped without cherry hover, which is Expert + set_rule(world.multiworld.get_location("Act Completion (Toilet of Doom)", world.player), + lambda state: can_use_hookshot(state, world) and can_hit(state, world) + and has_paintings(state, world, 1, False)) + + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 2", world.player), + lambda state: state.has("Snatcher's Contract - The Subcon Well", world.player)) + + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 3", world.player), + lambda state: state.has("Snatcher's Contract - Toilet of Doom", world.player)) + + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 4", world.player), + lambda state: state.has("Snatcher's Contract - Queen Vanessa's Manor", world.player)) + + add_rule(world.multiworld.get_entrance("Subcon Forest - Act 5", world.player), + lambda state: state.has("Snatcher's Contract - Mail Delivery Service", world.player)) + + if painting_logic(world): + add_rule(world.multiworld.get_location("Act Completion (Contractual Obligations)", world.player), + lambda state: has_paintings(state, world, 1, False)) + + for key in contract_locations: + if key == "Snatcher's Contract - The Subcon Well": + continue + + add_rule(world.multiworld.get_location(key, world.player), lambda state: has_paintings(state, world, 1)) + + +def set_alps_rules(world: World): + add_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), + lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.BREWING)) + + add_rule(world.multiworld.get_entrance("-> The Lava Cake", world.player), + lambda state: can_use_hookshot(state, world)) + + add_rule(world.multiworld.get_entrance("-> The Windmill", world.player), + lambda state: can_use_hookshot(state, world)) + + add_rule(world.multiworld.get_entrance("-> The Twilight Bell", world.player), + lambda state: can_use_hookshot(state, world) and can_use_hat(state, world, HatType.DWELLER)) + + add_rule(world.multiworld.get_location("Alpine Skyline - Mystifying Time Mesa: Zipline", world.player), + lambda state: can_use_hat(state, world, HatType.SPRINT) or can_use_hat(state, world, HatType.TIME_STOP)) + + add_rule(world.multiworld.get_entrance("Alpine Skyline - Finale", world.player), + lambda state: can_clear_alpine(state, world)) + + +def set_dlc1_rules(world: World): + add_rule(world.multiworld.get_entrance("Cruise Ship Entrance BV", world.player), + lambda state: can_use_hookshot(state, world)) + + # This particular item isn't present in Act 3 for some reason, yes in vanilla too + add_rule(world.multiworld.get_location("The Arctic Cruise - Toilet", world.player), + lambda state: state.can_reach("Bon Voyage!", "Region", world.player) + or state.can_reach("Ship Shape", "Region", world.player)) + + +def set_dlc2_rules(world: World): + add_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), + lambda state: state.has("Metro Ticket - Green", world.player) + or state.has("Metro Ticket - Blue", world.player)) + + add_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), + lambda state: state.has("Metro Ticket - Pink", world.player) + or state.has("Metro Ticket - Yellow", world.player) and state.has("Metro Ticket - Blue", world.player)) + + add_rule(world.multiworld.get_entrance("Nyakuza Metro - Finale", world.player), + lambda state: can_clear_metro(state, world)) + + add_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and state.has("Metro Ticket - Pink", world.player)) + + for key in shop_locations.keys(): + if "Green Clean Station Thug B" in key and is_location_valid(world, key): + add_rule(world.multiworld.get_location(key, world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player), "or") + + +def reg_act_connection(world: World, region: typing.Union[str, Region], unlocked_entrance: typing.Union[str, Entrance]): + reg: Region + entrance: Entrance + if isinstance(region, str): + reg = world.multiworld.get_region(region, world.player) + else: + reg = region + + if isinstance(unlocked_entrance, str): + entrance = world.multiworld.get_entrance(unlocked_entrance, world.player) + else: + entrance = unlocked_entrance + + world.multiworld.register_indirect_condition(reg, entrance) + + +# See randomize_act_entrances in Regions.py +# Called before set_rules +def set_rift_rules(world: World, regions: typing.Dict[str, Region]): + + # This is accessing the regions in place of these time rifts, so we can set the rules on all the entrances. + for entrance in regions["Time Rift - Gallery"].entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + + for entrance in regions["Time Rift - The Lab"].entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) + + for entrance in regions["Time Rift - Sewers"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) + reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 4", + world.player).connected_region, entrance) + + for entrance in regions["Time Rift - Bazaar"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6")) + reg_act_connection(world, world.multiworld.get_entrance("Mafia Town - Act 6", + world.player).connected_region, entrance) + + for entrance in regions["Time Rift - Mafia of Cooks"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger")) + + for entrance in regions["Time Rift - The Owl Express"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3")) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 2", + world.player).connected_region, entrance) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 3", + world.player).connected_region, entrance) + + for entrance in regions["Time Rift - The Moon"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5")) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 4", + world.player).connected_region, entrance) + reg_act_connection(world, world.multiworld.get_entrance("Battle of the Birds - Act 5", + world.player).connected_region, entrance) + + for entrance in regions["Time Rift - Dead Bird Studio"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Train")) + + for entrance in regions["Time Rift - Pipe"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) + reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 2", + world.player).connected_region, entrance) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) + + for entrance in regions["Time Rift - Village"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) + reg_act_connection(world, world.multiworld.get_entrance("Subcon Forest - Act 4", + world.player).connected_region, entrance) + + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) + + for entrance in regions["Time Rift - Sleepy Subcon"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO")) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 3)) + + for entrance in regions["Time Rift - Curly Tail Trail"].entrances: + add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player)) + + for entrance in regions["Time Rift - The Twilight Bell"].entrances: + add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player)) + + for entrance in regions["Time Rift - Alpine Skyline"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) + + if world.is_dlc1() > 0: + for entrance in regions["Time Rift - Balcony"].entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale")) + + for entrance in regions["Time Rift - Deep Sea"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) + + if world.is_dlc2() > 0: + for entrance in regions["Time Rift - Rumbi Factory"].entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) + + +# Basically the same as above, but without the need of the dict since we are just setting defaults +# Called if Act Rando is disabled +def set_default_rift_rules(world: World): + + for entrance in world.multiworld.get_region("Time Rift - Gallery", world.player).entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.BREWING) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.BIRDS))) + + for entrance in world.multiworld.get_region("Time Rift - The Lab", world.player).entrances: + add_rule(entrance, lambda state: can_use_hat(state, world, HatType.DWELLER) + and state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.ALPINE))) + + for entrance in world.multiworld.get_region("Time Rift - Sewers", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 4")) + reg_act_connection(world, "Down with the Mafia!", entrance.name) + + for entrance in world.multiworld.get_region("Time Rift - Bazaar", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Mafia Town - Act 6")) + reg_act_connection(world, "Heating Up Mafia Town", entrance.name) + + for entrance in world.multiworld.get_region("Time Rift - Mafia of Cooks", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Burger")) + + for entrance in world.multiworld.get_region("Time Rift - The Owl Express", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 2")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 3")) + reg_act_connection(world, "Murder on the Owl Express", entrance.name) + reg_act_connection(world, "Picture Perfect", entrance.name) + + for entrance in world.multiworld.get_region("Time Rift - The Moon", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 4")) + add_rule(entrance, lambda state: can_clear_act(state, world, "Battle of the Birds - Act 5")) + reg_act_connection(world, "Train Rush", entrance.name) + reg_act_connection(world, "The Big Parade", entrance.name) + + for entrance in world.multiworld.get_region("Time Rift - Dead Bird Studio", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Train")) + + for entrance in world.multiworld.get_region("Time Rift - Pipe", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 2")) + reg_act_connection(world, "The Subcon Well", entrance.name) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) + + for entrance in world.multiworld.get_region("Time Rift - Village", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "Subcon Forest - Act 4")) + reg_act_connection(world, "Queen Vanessa's Manor", entrance.name) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 2)) + + for entrance in world.multiworld.get_region("Time Rift - Sleepy Subcon", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "UFO")) + if painting_logic(world): + add_rule(entrance, lambda state: has_paintings(state, world, 3)) + + for entrance in world.multiworld.get_region("Time Rift - Curly Tail Trail", world.player).entrances: + add_rule(entrance, lambda state: state.has("Windmill Cleared", world.player)) + + for entrance in world.multiworld.get_region("Time Rift - The Twilight Bell", world.player).entrances: + add_rule(entrance, lambda state: state.has("Twilight Bell Cleared", world.player)) + + for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon")) + + if world.is_dlc1(): + for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances: + add_rule(entrance, lambda state: can_clear_act(state, world, "The Arctic Cruise - Finale")) + + for entrance in world.multiworld.get_region("Time Rift - Deep Sea", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Cake")) + + if world.is_dlc2(): + for entrance in world.multiworld.get_region("Time Rift - Rumbi Factory", world.player).entrances: + add_rule(entrance, lambda state: has_relic_combo(state, world, "Necklace")) + + +def set_event_rules(world: World): + for (name, data) in event_locs.items(): + if not is_location_valid(world, name): + continue + + if data.dlc_flags & HatDLC.death_wish and name in snatcher_coins.keys(): + name = f"{name} ({data.region})" + + event: Location = world.multiworld.get_location(name, world.player) + + if data.act_event: + add_rule(event, world.multiworld.get_location(f"Act Completion ({data.region})", world.player).access_rule) + + +def connect_regions(start_region: Region, exit_region: Region, entrancename: str, player: int) -> Entrance: + entrance = Entrance(player, entrancename, start_region) + start_region.exits.append(entrance) + entrance.connect(exit_region) + return entrance diff --git a/worlds/ahit/Types.py b/worlds/ahit/Types.py new file mode 100644 index 0000000000..16255d7ec5 --- /dev/null +++ b/worlds/ahit/Types.py @@ -0,0 +1,80 @@ +from enum import IntEnum, IntFlag +from typing import NamedTuple, Optional, List +from BaseClasses import Location, Item, ItemClassification + + +class HatInTimeLocation(Location): + game: str = "A Hat in Time" + + +class HatInTimeItem(Item): + game: str = "A Hat in Time" + + +class HatType(IntEnum): + NONE = -1 + SPRINT = 0 + BREWING = 1 + ICE = 2 + DWELLER = 3 + TIME_STOP = 4 + + +class HatDLC(IntFlag): + none = 0b000 + dlc1 = 0b001 + dlc2 = 0b010 + death_wish = 0b100 + dlc1_dw = 0b101 + dlc2_dw = 0b110 + + +class ChapterIndex(IntEnum): + SPACESHIP = 0 + MAFIA = 1 + BIRDS = 2 + SUBCON = 3 + ALPINE = 4 + FINALE = 5 + CRUISE = 6 + METRO = 7 + + +class Difficulty(IntEnum): + NORMAL = -1 + MODERATE = 0 + HARD = 1 + EXPERT = 2 + + +class LocData(NamedTuple): + id: Optional[int] = 0 + region: Optional[str] = "" + required_hats: Optional[List[HatType]] = [HatType.NONE] + hookshot: Optional[bool] = False + dlc_flags: Optional[HatDLC] = HatDLC.none + paintings: Optional[int] = 0 # Paintings required for Subcon painting shuffle + misc_required: Optional[List[str]] = [] + + # For UmbrellaLogic setting + umbrella: Optional[bool] = False # Umbrella required for this check + hit_requirement: Optional[int] = 0 # Hit required. 1 = Umbrella/Brewing only, 2 = bypass w/Dweller Mask (bells) + + # Other + act_event: Optional[bool] = False # Only used for event locations. Copy access rule from act completion + nyakuza_thug: Optional[str] = "" # Name of Nyakuza thug NPC (for metro shops) + + +class ItemData(NamedTuple): + code: Optional[int] + classification: ItemClassification + dlc_flags: Optional[HatDLC] = HatDLC.none + + +hat_type_to_item = { + HatType.SPRINT: "Sprint Hat", + HatType.BREWING: "Brewing Hat", + HatType.ICE: "Ice Hat", + HatType.DWELLER: "Dweller Mask", + HatType.TIME_STOP: "Time Stop Hat", +} diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py new file mode 100644 index 0000000000..0ed14c6376 --- /dev/null +++ b/worlds/ahit/__init__.py @@ -0,0 +1,334 @@ +from BaseClasses import Item, ItemClassification, Tutorial +from .Items import item_table, create_item, relic_groups, act_contracts, create_itempool +from .Regions import create_regions, randomize_act_entrances, chapter_act_info, create_events, get_shuffled_region +from .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID +from .Rules import set_rules +from .Options import ahit_options, slot_data_options, adjust_options +from .Types import HatType, ChapterIndex, HatInTimeItem +from .DeathWishLocations import create_dw_regions, dw_classes, death_wishes +from .DeathWishRules import set_dw_rules, create_enemy_events +from worlds.AutoWorld import World, WebWorld +from typing import List, Dict, TextIO +from worlds.LauncherComponents import Component, components, icon_paths +from Utils import local_path + +hat_craft_order: Dict[int, List[HatType]] = {} +hat_yarn_costs: Dict[int, Dict[HatType, int]] = {} +chapter_timepiece_costs: Dict[int, Dict[ChapterIndex, int]] = {} +excluded_dws: Dict[int, List[str]] = {} +excluded_bonuses: Dict[int, List[str]] = {} +dw_shuffle: Dict[int, List[str]] = {} +nyakuza_thug_items: Dict[int, Dict[str, int]] = {} +badge_seller_count: Dict[int, int] = {} + +components.append(Component("A Hat in Time Client", "AHITClient", icon='yatta')) +icon_paths['yatta'] = local_path('data', 'yatta.png') + + +class AWebInTime(WebWorld): + theme = "partyTime" + tutorials = [Tutorial( + "Multiworld Setup Guide", + "A guide for setting up A Hat in Time to be played in Archipelago.", + "English", + "ahit_en.md", + "setup/en", + ["CookieCat"] + )] + + +class HatInTimeWorld(World): + """ + A Hat in Time is a cute-as-peck 3D platformer featuring a little girl who stitches hats for wicked powers! + Freely explore giant worlds and recover Time Pieces to travel to new heights! + """ + + game = "A Hat in Time" + data_version = 1 + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = get_location_names() + + option_definitions = ahit_options + act_connections: Dict[str, str] = {} + shop_locs: List[str] = [] + item_name_groups = relic_groups + web = AWebInTime() + + def generate_early(self): + adjust_options(self) + + if self.multiworld.StartWithCompassBadge[self.player].value > 0: + self.multiworld.push_precollected(self.create_item("Compass Badge")) + + if self.is_dw_only(): + return + + # If our starting chapter is 4 and act rando isn't on, force hookshot into inventory + # If starting chapter is 3 and painting shuffle is enabled, and act rando isn't, give one free painting unlock + start_chapter: int = self.multiworld.StartingChapter[self.player].value + + if start_chapter == 4 or start_chapter == 3: + if self.multiworld.ActRandomizer[self.player].value == 0: + if start_chapter == 4: + self.multiworld.push_precollected(self.create_item("Hookshot Badge")) + + if start_chapter == 3 and self.multiworld.ShuffleSubconPaintings[self.player].value > 0: + self.multiworld.push_precollected(self.create_item("Progressive Painting Unlock")) + + def create_regions(self): + excluded_dws[self.player] = [] + excluded_bonuses[self.player] = [] + dw_shuffle[self.player] = [] + nyakuza_thug_items[self.player] = {} + badge_seller_count[self.player] = 0 + self.shop_locs = [] + self.topology_present = self.multiworld.ActRandomizer[self.player].value + + create_regions(self) + + if self.multiworld.EnableDeathWish[self.player].value > 0: + create_dw_regions(self) + + if self.is_dw_only(): + return + + create_events(self) + if self.is_dw(): + if "Snatcher's Hit List" not in self.get_excluded_dws() \ + or "Camera Tourist" not in self.get_excluded_dws(): + create_enemy_events(self) + + # place vanilla contract locations if contract shuffle is off + if self.multiworld.ShuffleActContracts[self.player].value == 0: + for name in contract_locations.keys(): + self.multiworld.get_location(name, self.player).place_locked_item(create_item(self, name)) + + def create_items(self): + hat_yarn_costs[self.player] = {HatType.SPRINT: -1, HatType.BREWING: -1, HatType.ICE: -1, + HatType.DWELLER: -1, HatType.TIME_STOP: -1} + + hat_craft_order[self.player] = [HatType.SPRINT, HatType.BREWING, HatType.ICE, + HatType.DWELLER, HatType.TIME_STOP] + + if self.multiworld.HatItems[self.player].value == 0 and self.multiworld.RandomizeHatOrder[self.player].value > 0: + self.random.shuffle(hat_craft_order[self.player]) + if self.multiworld.RandomizeHatOrder[self.player].value == 2: + hat_craft_order[self.player].remove(HatType.TIME_STOP) + hat_craft_order[self.player].append(HatType.TIME_STOP) + + self.multiworld.itempool += create_itempool(self) + + def set_rules(self): + self.act_connections = {} + chapter_timepiece_costs[self.player] = {ChapterIndex.MAFIA: -1, + ChapterIndex.BIRDS: -1, + ChapterIndex.SUBCON: -1, + ChapterIndex.ALPINE: -1, + ChapterIndex.FINALE: -1, + ChapterIndex.CRUISE: -1, + ChapterIndex.METRO: -1} + + if self.is_dw_only(): + # we already have all items if this is the case, no need for rules + self.multiworld.push_precollected(HatInTimeItem("Death Wish Only Mode", ItemClassification.progression, + None, self.player)) + + self.multiworld.completion_condition[self.player] = lambda state: state.has("Death Wish Only Mode", + self.player) + + if self.multiworld.DWEnableBonus[self.player].value == 0: + for name in death_wishes: + if name == "Snatcher Coins in Nyakuza Metro" and not self.is_dlc2(): + continue + + if self.multiworld.DWShuffle[self.player].value > 0 and name not in self.get_dw_shuffle(): + continue + + full_clear = self.multiworld.get_location(f"{name} - All Clear", self.player) + full_clear.address = None + full_clear.place_locked_item(HatInTimeItem("Nothing", ItemClassification.filler, None, self.player)) + full_clear.show_in_spoiler = False + + return + + if self.multiworld.ActRandomizer[self.player].value > 0: + randomize_act_entrances(self) + + set_rules(self) + + if self.is_dw(): + set_dw_rules(self) + + def create_item(self, name: str) -> Item: + return create_item(self, name) + + def fill_slot_data(self) -> dict: + slot_data: dict = {"Chapter1Cost": chapter_timepiece_costs[self.player][ChapterIndex.MAFIA], + "Chapter2Cost": chapter_timepiece_costs[self.player][ChapterIndex.BIRDS], + "Chapter3Cost": chapter_timepiece_costs[self.player][ChapterIndex.SUBCON], + "Chapter4Cost": chapter_timepiece_costs[self.player][ChapterIndex.ALPINE], + "Chapter5Cost": chapter_timepiece_costs[self.player][ChapterIndex.FINALE], + "Chapter6Cost": chapter_timepiece_costs[self.player][ChapterIndex.CRUISE], + "Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO], + "BadgeSellerItemCount": badge_seller_count[self.player], + "SeedNumber": str(self.multiworld.seed), # For shop prices + "SeedName": self.multiworld.seed_name} + + if self.multiworld.HatItems[self.player].value == 0: + slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT]) + slot_data.setdefault("BrewingYarnCost", hat_yarn_costs[self.player][HatType.BREWING]) + slot_data.setdefault("IceYarnCost", hat_yarn_costs[self.player][HatType.ICE]) + slot_data.setdefault("DwellerYarnCost", hat_yarn_costs[self.player][HatType.DWELLER]) + slot_data.setdefault("TimeStopYarnCost", hat_yarn_costs[self.player][HatType.TIME_STOP]) + slot_data.setdefault("Hat1", int(hat_craft_order[self.player][0])) + slot_data.setdefault("Hat2", int(hat_craft_order[self.player][1])) + slot_data.setdefault("Hat3", int(hat_craft_order[self.player][2])) + slot_data.setdefault("Hat4", int(hat_craft_order[self.player][3])) + slot_data.setdefault("Hat5", int(hat_craft_order[self.player][4])) + + if self.multiworld.ActRandomizer[self.player].value > 0: + for name in self.act_connections.keys(): + slot_data[name] = self.act_connections[name] + + if self.is_dlc2() and not self.is_dw_only(): + for name in nyakuza_thug_items[self.player].keys(): + slot_data[name] = nyakuza_thug_items[self.player][name] + + if self.is_dw(): + i: int = 0 + for name in excluded_dws[self.player]: + if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal": + continue + + slot_data[f"excluded_dw{i}"] = dw_classes[name] + i += 1 + + i = 0 + if self.multiworld.DWAutoCompleteBonuses[self.player].value == 0: + for name in excluded_bonuses[self.player]: + if name in excluded_dws[self.player]: + continue + + slot_data[f"excluded_bonus{i}"] = dw_classes[name] + i += 1 + + if self.multiworld.DWShuffle[self.player].value > 0: + shuffled_dws = self.get_dw_shuffle() + for i in range(len(shuffled_dws)): + slot_data[f"dw_{i}"] = dw_classes[shuffled_dws[i]] + + for option_name in slot_data_options: + option = getattr(self.multiworld, option_name)[self.player] + slot_data[option_name] = option.value + + return slot_data + + def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]): + if self.is_dw_only() or self.multiworld.ActRandomizer[self.player].value == 0: + return + + new_hint_data = {} + alpine_regions = ["The Birdhouse", "The Lava Cake", "The Windmill", "The Twilight Bell", "Alpine Skyline Area"] + metro_regions = ["Yellow Overpass Station", "Green Clean Station", "Bluefin Tunnel", "Pink Paw Station"] + + for key, data in location_table.items(): + if not is_location_valid(self, key): + continue + + location = self.multiworld.get_location(key, self.player) + region_name: str + + if data.region in alpine_regions: + region_name = "Alpine Free Roam" + elif data.region in metro_regions: + region_name = "Nyakuza Free Roam" + elif data.region in chapter_act_info.keys(): + region_name = location.parent_region.name + else: + continue + + new_hint_data[location.address] = get_shuffled_region(self, region_name) + + if self.is_dlc1() and self.multiworld.Tasksanity[self.player].value > 0: + ship_shape_region = get_shuffled_region(self, "Ship Shape") + id_start: int = TASKSANITY_START_ID + for i in range(self.multiworld.TasksanityCheckCount[self.player].value): + new_hint_data[id_start+i] = ship_shape_region + + hint_data[self.player] = new_hint_data + + def write_spoiler_header(self, spoiler_handle: TextIO): + for i in self.get_chapter_costs(): + spoiler_handle.write("Chapter %i Cost: %i\n" % (i, self.get_chapter_costs()[ChapterIndex(i)])) + + for hat in hat_craft_order[self.player]: + spoiler_handle.write("Hat Cost: %s: %i\n" % (hat, hat_yarn_costs[self.player][hat])) + + def set_chapter_cost(self, chapter: ChapterIndex, cost: int): + chapter_timepiece_costs[self.player][chapter] = cost + + def get_chapter_cost(self, chapter: ChapterIndex) -> int: + return chapter_timepiece_costs[self.player][chapter] + + def get_hat_craft_order(self): + return hat_craft_order[self.player] + + def get_hat_yarn_costs(self): + return hat_yarn_costs[self.player] + + def get_chapter_costs(self): + return chapter_timepiece_costs[self.player] + + def is_dlc1(self) -> bool: + return self.multiworld.EnableDLC1[self.player].value > 0 + + def is_dlc2(self) -> bool: + return self.multiworld.EnableDLC2[self.player].value > 0 + + def is_dw(self) -> bool: + return self.multiworld.EnableDeathWish[self.player].value > 0 + + def is_dw_only(self) -> bool: + return self.is_dw() and self.multiworld.DeathWishOnly[self.player].value > 0 + + def get_excluded_dws(self): + return excluded_dws[self.player] + + def get_excluded_bonuses(self): + return excluded_bonuses[self.player] + + def is_dw_excluded(self, name: str) -> bool: + # don't exclude Seal the Deal if it's our goal + if self.multiworld.EndGoal[self.player].value == 3 and name == "Seal the Deal" \ + and f"{name} - Main Objective" not in self.multiworld.exclude_locations[self.player]: + return False + + if name in excluded_dws[self.player]: + return True + + return f"{name} - Main Objective" in self.multiworld.exclude_locations[self.player] + + def is_bonus_excluded(self, name: str) -> bool: + if self.is_dw_excluded(name) or name in excluded_bonuses[self.player]: + return True + + return f"{name} - All Clear" in self.multiworld.exclude_locations[self.player] + + def get_dw_shuffle(self): + return dw_shuffle[self.player] + + def set_dw_shuffle(self, shuffle: List[str]): + dw_shuffle[self.player] = shuffle + + def get_badge_seller_count(self) -> int: + return badge_seller_count[self.player] + + def set_badge_seller_count(self, value: int): + badge_seller_count[self.player] = value + + def get_nyakuza_thug_items(self): + return nyakuza_thug_items[self.player] + + def set_nyakuza_thug_items(self, items: Dict[str, int]): + nyakuza_thug_items[self.player] = items diff --git a/worlds/ahit/docs/en_A Hat in Time.md b/worlds/ahit/docs/en_A Hat in Time.md new file mode 100644 index 0000000000..c4a4341763 --- /dev/null +++ b/worlds/ahit/docs/en_A Hat in Time.md @@ -0,0 +1,31 @@ +# A Hat in Time + +## Where is the settings page? + +The [player settings page for this game](../player-settings) contains all the options you need to configure and export a +config file. + +## What does randomization do to this game? + +Items which the player would normally acquire throughout the game have been moved around. Chapter costs are randomized in a progressive order based on your settings, so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. If act shuffle is turned on, the levels and Time Rifts in these chapters will be randomized as well. + +To unlock and access a chapter's Time Rift in act shuffle, the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed, and then you must enter a level that allows you to enter that Time Rift. For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game. To unlock this Time Rift in act shuffle (and therefore the level it contains) you must complete the level that was shuffled in place of Heating Up Mafia Town and then enter the Time Rift through a Mafia Town level. + +## What items and locations get shuffled? + +Time Pieces, Relics, Yarn, Badges, and most other items are shuffled. Unlike in the vanilla game, yarn is typeless, and will be automatically crafted in a set order once you gather enough yarn for each hat. Any items in the world, shops, act completions, and optionally storybook pages or Death Wish contracts are the locations. + +Any freestanding items that are considered to be progression or useful will have a rainbow streak particle attached to them. Filler items will have a white glow attached to them instead. + +## Which items can be in another player's world? + +Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit +certain items to your own world. + +## What does another world's item look like in A Hat in Time? + +Items belonging to other worlds are represented by a badge with the Archipelago logo on it. + +## When the player receives an item, what happens? + +When the player receives an item, it will play the item collect effect and information about the item will be printed on the screen and in the in-game developer console. diff --git a/worlds/ahit/docs/setup_en.md b/worlds/ahit/docs/setup_en.md new file mode 100644 index 0000000000..d2db2fe47f --- /dev/null +++ b/worlds/ahit/docs/setup_en.md @@ -0,0 +1,43 @@ +# Setup Guide for A Hat in Time in Archipelago + +## Required Software +- [Steam release of A Hat in Time](https://store.steampowered.com/app/253230/A_Hat_in_Time/) + +- [Archipelago Workshop Mod for A Hat in Time](https://steamcommunity.com/sharedfiles/filedetails/?id=3026842601) + + +## Instructions + +1. Have Steam running. Open the Steam console with [this link.](steam://open/console) + +2. In the Steam console, enter the following command: +`download_depot 253230 253232 7770543545116491859`. Wait for the console to say the download is finished. + +3. Once the download finishes, go to `steamapps/content/app_253230` in Steam's program folder. + +4. There should be a folder named `depot_253232`. Rename it to HatinTime_AP and move it to your `steamapps/common` folder. + +5. In the HatinTime_AP folder, navigate to `Binaries/Win64` and create a new file: `steam_appid.txt`. In this new text file, input the number **253230** on the first line. + +6. Create a shortcut of `HatinTimeGame.exe` from that folder and move it to wherever you'd like. You will use this shortcut to open the Archipelago-compatible version of A Hat in Time. + +7. Start up the game using your new shortcut. To confirm if you are on the correct version, go to Settings -> Game Settings. If you don't see an option labelled ***Live Game Events*** you should be running the correct version of the game. In Game Settings, make sure ***Enable Developer Console*** is checked. + + + +## Connecting to the Archipelago server + +When you create a new save file, you should be prompted to enter your slot name, password, and Archipelago server address:port after loading into the Spaceship. Once that's done, the game will automatically connect to the multiserver using the info you entered whenever that save file is loaded. If you must change the IP or port for the save file, use the `ap_set_connection_info` console command. + + +## Console Commands + +Commands will not work on the title screen, you must be in-game to use them. To use console commands, make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game. + +`ap_say ` - Send a chat message to the server. Supports commands, such as !hint or !release. + +`ap_deathlink` - Toggle Death Link. + +`ap_set_connection_info ` - Set the connection info for the save file. The IP address MUST BE IN QUOTES! + +`ap_show_connection_info` - Show the connection info for the save file. \ No newline at end of file diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py new file mode 100644 index 0000000000..7c2b9783e6 --- /dev/null +++ b/worlds/ahit/test/TestActs.py @@ -0,0 +1,31 @@ +from worlds.ahit.Regions import act_chapters +from worlds.ahit.test.TestBase import HatInTimeTestBase + + +class TestActs(HatInTimeTestBase): + def run_default_tests(self) -> bool: + return False + + def testAllStateCanReachEverything(self): + pass + + options = { + "ActRandomizer": 2, + "EnableDLC1": 1, + "EnableDLC2": 1, + "ShuffleActContracts": 0, + } + + def test_act_shuffle(self): + for i in range(1000): + self.world_setup() + self.collect_all_but([""]) + + for name in act_chapters.keys(): + region = self.multiworld.get_region(name, 1) + for entrance in region.entrances: + self.assertTrue(self.can_reach_entrance(entrance.name), + f"Can't reach {name} from {entrance}\n" + f"{entrance.parent_region.entrances[0]} -> {entrance.parent_region} " + f"-> {entrance} -> {name}" + f" (expected method of access)") diff --git a/worlds/ahit/test/TestBase.py b/worlds/ahit/test/TestBase.py new file mode 100644 index 0000000000..1eb4dd6555 --- /dev/null +++ b/worlds/ahit/test/TestBase.py @@ -0,0 +1,5 @@ +from test.TestBase import WorldTestBase + + +class HatInTimeTestBase(WorldTestBase): + game = "A Hat in Time" diff --git a/worlds/ahit/test/__init__.py b/worlds/ahit/test/__init__.py new file mode 100644 index 0000000000..e69de29bb2