diff --git a/.gitignore b/.gitignore index 26885103..d859d4f8 100644 --- a/.gitignore +++ b/.gitignore @@ -155,4 +155,7 @@ Archipelago.zip #minecraft server stuff jdk*/ -minecraft*/ \ No newline at end of file +minecraft*/ + +#pyenv +.python-version diff --git a/README.md b/README.md index 739a8caf..35224128 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ Currently, the following games are supported: * Super Metroid * Secret of Evermore * Final Fantasy +* Rogue Legacy For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/). Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled diff --git a/WebHostLib/static/assets/gameInfo/en_Rogue Legacy.md b/WebHostLib/static/assets/gameInfo/en_Rogue Legacy.md new file mode 100644 index 00000000..f7c1068d --- /dev/null +++ b/WebHostLib/static/assets/gameInfo/en_Rogue Legacy.md @@ -0,0 +1,22 @@ +# Rogue Legacy (PC) + +## Where is the settings page? +The player settings page for this game is located here. It contains all the options +you need to configure and export a config file. + +## What does randomization do to this game? +You are not able to buy skill upgrades in the manor upgrade screen, and instead, need to find them in order to level up +your character to make fighting the 5 bosses easier. + +## What items and locations get shuffled? +All the skill upgrades, class upgrades, runes packs, and equipment packs are shuffled in the manor upgrade screen, +diary checks, chests and fairy chests, and boss rewards. Skill upgrades are also grouped in packs of 5 to make the +finding of stats less of a chore. Runes and Equipment are also grouped together. + +## 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. + +## When the player receives an item, what happens? +When the player receives an item, your character will hold the item above their head and display it to the world. It's +good for business! diff --git a/WebHostLib/static/assets/tutorial/legacy/legacy_en.md b/WebHostLib/static/assets/tutorial/legacy/legacy_en.md new file mode 100644 index 00000000..65f9d704 --- /dev/null +++ b/WebHostLib/static/assets/tutorial/legacy/legacy_en.md @@ -0,0 +1,47 @@ +# Rogue Legacy Randomizer Setup Guide + +## Required Software + +- [Rogue Legacy Randomizer](https://github.com/ThePhar/RogueLegacyRandomizer/releases) + +## Configuring your YAML file + +### What is a YAML file and why do I need one? +Your YAML file contains a set of configuration options which provide the generator with information about how +it should generate your game. Each player of a multiworld will provide their own YAML file. This setup allows +each player to enjoy an experience customized for their taste, and different players in the same multiworld +can all have different options. + +### Where do I get a YAML file? +you can customize your settings by visiting the rogue legacy settings page here. + +### Connect to the MultiServer +Once in game, press the start button and the AP connection screen should appear. You will fill out the hostname, port, +slot name, and password (if applicable). You should only need to fill out hostname, port, and password if the server +provides an alternative one to the default values. + +### Play the game +Once you have entered the required values, you go to Connect and then select Confirm on the "Ready to Start" screen. +Now you're off to start your legacy! + +## Manual Installation +In order to run Rogue Legacy Randomizer you will need to have Rogue Legacy installed on your local machine. Extract the +Randomizer release into a desired folder **outside** of your Rogue Legacy install. Copy the following files from your +Rogue Legacy install into the main directory of your Rogue Legacy Randomizer install: + +- DS2DEngine.dll +- InputSystem.dll +- Nuclex.Input.dll +- SpriteSystem.dll +- Tweener.dll + +And copy the directory from your Rogue Legacy install as well into the main directory of your Rogue Legacy Randomizer +install: + +- Content/ + +Then copy the contents of the CustomContent directory in your Rogue Legacy Randomizer into the newly copied Content +directory and overwrite all files. + +**BE SURE YOU ARE REPLACING THE COPIED FILES IN YOUR ROGUE LEGACY RANDOMIZER DIRECTORY AND NOT REPLACING YOUR ROGUE +LEGACY FILES!** diff --git a/WebHostLib/static/assets/tutorial/tutorials.json b/WebHostLib/static/assets/tutorial/tutorials.json index 67f33e16..eb27113d 100644 --- a/WebHostLib/static/assets/tutorial/tutorials.json +++ b/WebHostLib/static/assets/tutorial/tutorials.json @@ -342,5 +342,24 @@ ] } ] + }, + { + "gameTitle": "Rogue Legacy", + "tutorials": [ + { + "name": "Multiworld Setup Guide", + "description": "A guide to setting up the Rogue Legacy Randomizer software on your computer. This guide covers single-player, multiworld, and related software.", + "files": [ + { + "language": "English", + "filename": "legacy/legacy_en.md", + "link": "legacy/legacy/en", + "authors": [ + "Phar" + ] + } + ] + } + ] } ] diff --git a/worlds/legacy/Items.py b/worlds/legacy/Items.py new file mode 100644 index 00000000..e134444b --- /dev/null +++ b/worlds/legacy/Items.py @@ -0,0 +1,129 @@ +import typing + +from BaseClasses import Item +from .Names import ItemName + + +class ItemData(typing.NamedTuple): + code: typing.Optional[int] + progression: bool + quantity: int = 1 + event: bool = False + + +class LegacyItem(Item): + game: str = "Rogue Legacy" + + def __init__(self, name, advancement: bool = False, code: int = None, player: int = None): + super(LegacyItem, self).__init__(name, advancement, code, player) + + +# Separate tables for each type of item. +vendors_table = { + ItemName.blacksmith: ItemData(90000, True), + ItemName.enchantress: ItemData(90001, True), + ItemName.architect: ItemData(90002, False), +} + +static_classes_table = { + ItemName.knight: ItemData(90080, True), + ItemName.paladin: ItemData(90081, True), + ItemName.mage: ItemData(90082, True), + ItemName.archmage: ItemData(90083, True), + ItemName.barbarian: ItemData(90084, True), + ItemName.barbarian_king: ItemData(90085, True), + ItemName.knave: ItemData(90086, True), + ItemName.assassin: ItemData(90087, True), + ItemName.shinobi: ItemData(90088, True), + ItemName.hokage: ItemData(90089, True), + ItemName.miner: ItemData(90090, True), + ItemName.spelunker: ItemData(90091, True), + ItemName.lich: ItemData(90092, True), + ItemName.lich_king: ItemData(90093, True), + ItemName.spellthief: ItemData(90094, True), + ItemName.spellsword: ItemData(90095, True), + ItemName.dragon: ItemData(90096, True), + ItemName.traitor: ItemData(90097, True), +} + +progressive_classes_table = { + ItemName.progressive_knight: ItemData(90003, True, 2), + ItemName.progressive_mage: ItemData(90004, True, 2), + ItemName.progressive_barbarian: ItemData(90005, True, 2), + ItemName.progressive_knave: ItemData(90006, True, 2), + ItemName.progressive_shinobi: ItemData(90007, True, 2), + ItemName.progressive_miner: ItemData(90008, True, 2), + ItemName.progressive_lich: ItemData(90009, True, 2), + ItemName.progressive_spellthief: ItemData(90010, True, 2), +} + +skill_unlocks_table = { + ItemName.health: ItemData(90013, True, 15), + ItemName.mana: ItemData(90014, True, 15), + ItemName.attack: ItemData(90015, True, 15), + ItemName.magic_damage: ItemData(90016, True, 15), + ItemName.armor: ItemData(90017, True, 10), + ItemName.equip: ItemData(90018, True, 10), + ItemName.crit_chance: ItemData(90019, False, 5), + ItemName.crit_damage: ItemData(90020, False, 5), + ItemName.down_strike: ItemData(90021, False), + ItemName.gold_gain: ItemData(90022, False), + ItemName.potion_efficiency: ItemData(90023, False), + ItemName.invulnerability_time: ItemData(90024, False), + ItemName.mana_cost_down: ItemData(90025, False), + ItemName.death_defiance: ItemData(90026, False), + ItemName.haggling: ItemData(90027, False), + ItemName.random_children: ItemData(90028, False), +} + +blueprints_table = { + ItemName.squire_blueprints: ItemData(90040, True), + ItemName.silver_blueprints: ItemData(90041, True), + ItemName.guardian_blueprints: ItemData(90042, True), + ItemName.imperial_blueprints: ItemData(90043, True), + ItemName.royal_blueprints: ItemData(90044, True), + ItemName.knight_blueprints: ItemData(90045, True), + ItemName.ranger_blueprints: ItemData(90046, True), + ItemName.sky_blueprints: ItemData(90047, True), + ItemName.dragon_blueprints: ItemData(90048, True), + ItemName.slayer_blueprints: ItemData(90049, True), + ItemName.blood_blueprints: ItemData(90050, True), + ItemName.sage_blueprints: ItemData(90051, True), + ItemName.retribution_blueprints: ItemData(90052, True), + ItemName.holy_blueprints: ItemData(90053, True), + ItemName.dark_blueprints: ItemData(90054, True), +} + +runes_table = { + ItemName.vault_runes: ItemData(90060, True), + ItemName.sprint_runes: ItemData(90061, True), + ItemName.vampire_runes: ItemData(90062, True), + ItemName.sky_runes: ItemData(90063, True), + ItemName.siphon_runes: ItemData(90064, True), + ItemName.retaliation_runes: ItemData(90065, True), + ItemName.bounty_runes: ItemData(90066, True), + ItemName.haste_runes: ItemData(90067, True), + ItemName.curse_runes: ItemData(90068, True), + ItemName.grace_runes: ItemData(90069, True), + ItemName.balance_runes: ItemData(90070, True), +} + +misc_items_table = { + ItemName.trip_stat_increase: ItemData(90030, False), + ItemName.gold_1000: ItemData(90031, False), + ItemName.gold_3000: ItemData(90032, False), + ItemName.gold_5000: ItemData(90033, False), +} + +# Complete item table. +item_table = { + **vendors_table, + **static_classes_table, + **progressive_classes_table, + **skill_unlocks_table, + **blueprints_table, + **runes_table, + **misc_items_table, +} + +lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in item_table.items() if data.code} diff --git a/worlds/legacy/Locations.py b/worlds/legacy/Locations.py new file mode 100644 index 00000000..8e256c0f --- /dev/null +++ b/worlds/legacy/Locations.py @@ -0,0 +1,85 @@ +import typing + +from BaseClasses import Location +from .Names import LocationName + + +class LegacyLocation(Location): + game: str = "Rogue Legacy" + + +base_location_table = { + # Manor Renovations + LocationName.manor_ground_base: 91000, + LocationName.manor_main_base: 91001, + LocationName.manor_main_bottom_window: 91002, + LocationName.manor_main_top_window: 91003, + LocationName.manor_main_roof: 91004, + LocationName.manor_left_wing_base: 91005, + LocationName.manor_left_wing_window: 91006, + LocationName.manor_left_wing_roof: 91007, + LocationName.manor_left_big_base: 91008, + LocationName.manor_left_big_upper1: 91009, + LocationName.manor_left_big_upper2: 91010, + LocationName.manor_left_big_windows: 91011, + LocationName.manor_left_big_roof: 91012, + LocationName.manor_left_far_base: 91013, + LocationName.manor_left_far_roof: 91014, + LocationName.manor_left_extension: 91015, + LocationName.manor_left_tree1: 91016, + LocationName.manor_left_tree2: 91017, + LocationName.manor_right_wing_base: 91018, + LocationName.manor_right_wing_window: 91019, + LocationName.manor_right_wing_roof: 91020, + LocationName.manor_right_big_base: 91021, + LocationName.manor_right_big_upper: 91022, + LocationName.manor_right_big_roof: 91023, + LocationName.manor_right_high_base: 91024, + LocationName.manor_right_high_upper: 91025, + LocationName.manor_right_high_tower: 91026, + LocationName.manor_right_extension: 91027, + LocationName.manor_right_tree: 91028, + LocationName.manor_observatory_base: 91029, + LocationName.manor_observatory_scope: 91030, + + # Boss Rewards + LocationName.boss_khindr: 91100, + LocationName.boss_alexander: 91102, + LocationName.boss_leon: 91104, + LocationName.boss_herodotus: 91106, + + # Special Rooms + LocationName.special_jukebox: 91200, + + # Special Locations + LocationName.castle: None, + LocationName.garden: None, + LocationName.tower: None, + LocationName.dungeon: None, + LocationName.fountain: None, +} + +diary_location_table = {f"{LocationName.diary} {i + 1}": i + 91300 for i in range(0, 25)} + +fairy_chest_location_table = { + **{f"{LocationName.castle} - Fairy Chest {i + 1}": i + 91400 for i in range(0, 50)}, + **{f"{LocationName.garden} - Fairy Chest {i + 1}": i + 91450 for i in range(0, 50)}, + **{f"{LocationName.tower} - Fairy Chest {i + 1}": i + 91500 for i in range(0, 50)}, + **{f"{LocationName.dungeon} - Fairy Chest {i + 1}": i + 91550 for i in range(0, 50)}, +} + +chest_location_table = { + **{f"{LocationName.castle} - Chest {i + 1}": i + 91600 for i in range(0, 100)}, + **{f"{LocationName.garden} - Chest {i + 1}": i + 91700 for i in range(0, 100)}, + **{f"{LocationName.tower} - Chest {i + 1}": i + 91800 for i in range(0, 100)}, + **{f"{LocationName.dungeon} - Chest {i + 1}": i + 91900 for i in range(0, 100)}, +} + +location_table = { + **base_location_table, + **diary_location_table, + **fairy_chest_location_table, + **chest_location_table, +} + +lookup_id_to_name: typing.Dict[int, str] = {id: name for name, _ in location_table.items()} diff --git a/worlds/legacy/Names/ItemName.py b/worlds/legacy/Names/ItemName.py new file mode 100644 index 00000000..474f8ef7 --- /dev/null +++ b/worlds/legacy/Names/ItemName.py @@ -0,0 +1,95 @@ +# Vendor Definitions +blacksmith = "Blacksmith" +enchantress = "Enchantress" +architect = "Architect" + +# Progressive Class Definitions +progressive_knight = "Progressive Knights" +progressive_mage = "Progressive Mages" +progressive_barbarian = "Progressive Barbarians" +progressive_knave = "Progressive Knaves" +progressive_shinobi = "Progressive Shinobis" +progressive_miner = "Progressive Miners" +progressive_lich = "Progressive Liches" +progressive_spellthief = "Progressive Spellthieves" + +# Static Class Definitions +knight = "Knights" +paladin = "Paladins" +mage = "Mages" +archmage = "Archmages" +barbarian = "Barbarians" +barbarian_king = "Barbarian Kings" +knave = "Knaves" +assassin = "Assassins" +shinobi = "Shinobis" +hokage = "Hokages" +miner = "Miners" +spelunker = "Spelunkers" +lich = "Lichs" +lich_king = "Lich Kings" +spellthief = "Spellthieves" +spellsword = "Spellswords" +dragon = "Dragons" +traitor = "Traitors" + +# Skill Unlock Definitions +health = "Health Up" +mana = "Mana Up" +attack = "Attack Up" +magic_damage = "Magic Damage Up" +armor = "Armor Up" +equip = "Equip Up" +crit_chance = "Crit Chance Up" +crit_damage = "Crit Damage Up" +down_strike = "Down Strike Up" +gold_gain = "Gold Gain Up" +potion_efficiency = "Potion Efficiency Up" +invulnerability_time = "Invulnerability Time Up" +mana_cost_down = "Mana Cost Down" +death_defiance = "Death Defiance" +haggling = "Haggling" +random_children = "Randomize Children" + +# Misc. Definitions +trip_stat_increase = "Triple Stat Increase" +gold_1000 = "1000 Gold" +gold_3000 = "3000 Gold" +gold_5000 = "5000 Gold" + +# Blueprint Definitions +squire_blueprints = "Squire Armor Blueprints" +silver_blueprints = "Silver Armor Blueprints" +guardian_blueprints = "Guardian Armor Blueprints" +imperial_blueprints = "Imperial Armor Blueprints" +royal_blueprints = "Royal Armor Blueprints" +knight_blueprints = "Knight Armor Blueprints" +ranger_blueprints = "Ranger Armor Blueprints" +sky_blueprints = "Sky Armor Blueprints" +dragon_blueprints = "Dragon Armor Blueprints" +slayer_blueprints = "Slayer Armor Blueprints" +blood_blueprints = "Blood Armor Blueprints" +sage_blueprints = "Sage Armor Blueprints" +retribution_blueprints = "Retribution Armor Blueprints" +holy_blueprints = "Holy Armor Blueprints" +dark_blueprints = "Dark Armor Blueprints" + +# Rune Definitions +vault_runes = "Vault Runes" +sprint_runes = "Sprint Runes" +vampire_runes = "Vampire Runes" +sky_runes = "Sky Runes" +siphon_runes = "Siphon Runes" +retaliation_runes = "Retaliation Runes" +bounty_runes = "Bounty Runes" +haste_runes = "Haste Runes" +curse_runes = "Curse Runes" +grace_runes = "Grace Runes" +balance_runes = "Balance Runes" + +# Event Definitions +boss_khindr = "Defeat Khindr" +boss_alexander = "Defeat Alexander" +boss_leon = "Defeat Ponce de Leon" +boss_herodotus = "Defeat Herodotus" +boss_fountain = "Defeat The Fountain" diff --git a/worlds/legacy/Names/LocationName.py b/worlds/legacy/Names/LocationName.py new file mode 100644 index 00000000..f6699907 --- /dev/null +++ b/worlds/legacy/Names/LocationName.py @@ -0,0 +1,52 @@ +# Manor Piece Definitions +manor_ground_base = "Manor Renovation - Ground Road" +manor_main_base = "Manor Renovation - Main Base" +manor_main_bottom_window = "Manor Renovation - Main Bottom Window" +manor_main_top_window = "Manor Renovation - Main Top Window" +manor_main_roof = "Manor Renovation - Main Rooftop" +manor_left_wing_base = "Manor Renovation - Left Wing Base" +manor_left_wing_window = "Manor Renovation - Left Wing Window" +manor_left_wing_roof = "Manor Renovation - Left Wing Rooftop" +manor_left_big_base = "Manor Renovation - Left Big Base" +manor_left_big_upper1 = "Manor Renovation - Left Big Upper 1" +manor_left_big_upper2 = "Manor Renovation - Left Big Upper 2" +manor_left_big_windows = "Manor Renovation - Left Big Windows" +manor_left_big_roof = "Manor Renovation - Left Big Rooftop" +manor_left_far_base = "Manor Renovation - Left Far Base" +manor_left_far_roof = "Manor Renovation - Left Far Roof" +manor_left_extension = "Manor Renovation - Left Extension" +manor_left_tree1 = "Manor Renovation - Left Tree 1" +manor_left_tree2 = "Manor Renovation - Left Tree 2" +manor_right_wing_base = "Manor Renovation - Right Wing Base" +manor_right_wing_window = "Manor Renovation - Right Wing Window" +manor_right_wing_roof = "Manor Renovation - Right Wing Rooftop" +manor_right_big_base = "Manor Renovation - Right Big Base" +manor_right_big_upper = "Manor Renovation - Right Big Upper" +manor_right_big_roof = "Manor Renovation - Right Big Rooftop" +manor_right_high_base = "Manor Renovation - Right High Base" +manor_right_high_upper = "Manor Renovation - Right High Upper" +manor_right_high_tower = "Manor Renovation - Right High Tower" +manor_right_extension = "Manor Renovation - Right Extension" +manor_right_tree = "Manor Renovation - Right Tree" +manor_observatory_base = "Manor Renovation - Observatory Base" +manor_observatory_scope = "Manor Renovation - Observatory Telescope" + +# Boss Chest Definitions +boss_khindr = "Khindr's Boss Chest" +boss_alexander = "Alexander's Boss Chest" +boss_leon = "Ponce de Leon's Boss Chest" +boss_herodotus = "Herodotus's Boss Chest" + +# Special Room Definitions +special_jukebox = "Jukebox" + +# Shorthand Definitions +diary = "Diary" + +# Region Definitions +outside = "Outside Castle Hamson" +castle = "Castle Hamson" +garden = "Forest Abkhazia" +tower = "The Maya" +dungeon = "The Land of Darkness" +fountain = "Fountain Room" diff --git a/worlds/legacy/Options.py b/worlds/legacy/Options.py new file mode 100644 index 00000000..c46aa207 --- /dev/null +++ b/worlds/legacy/Options.py @@ -0,0 +1,128 @@ +import typing + +from Options import Choice, Range, Option, Toggle, DeathLink, DefaultOnToggle + + +class StartingGender(Choice): + """ + Determines the gender of your initial 'Sir Lee' character. + """ + displayname = "Starting Gender" + option_sir = 0 + option_lady = 1 + alias_male = 0 + alias_female = 1 + default = 0 + + +class StartingClass(Choice): + """ + Determines the starting class of your initial 'Sir Lee' character. + """ + displayname = "Starting Class" + option_knight = 0 + option_mage = 1 + option_barbarian = 2 + option_knave = 3 + default = 0 + + +class NewGamePlus(Choice): + """ + Puts the castle in new game plus mode which vastly increases enemy level, but increases gold gain by 50%. Not + recommended for those inexperienced to Rogue Legacy! + """ + displayname = "New Game Plus" + option_normal = 0 + option_new_game_plus = 1 + option_new_game_plus_2 = 2 + alias_hard = 1 + alias_brutal = 2 + default = 0 + + +class FairyChestsPerZone(Range): + """ + Determines the number of Fairy Chests in a given zone that contain items. After these have been checked, only stat + bonuses can be found in Fairy Chests. + """ + displayname = "Fairy Chests Per Zone" + range_start = 5 + range_end = 15 + default = 5 + + +class ChestsPerZone(Range): + """ + Determines the number of Non-Fairy Chests in a given zone that contain items. After these have been checked, only + gold or stat bonuses can be found in Chests. + """ + displayname = "Chests Per Zone" + range_start = 15 + range_end = 30 + default = 15 + + +class Vendors(Choice): + """ + Determines where to place the Blacksmith and Enchantress unlocks in logic (or start with them unlocked). + """ + displayname = "Vendors" + option_start_unlocked = 0 + option_early = 1 + option_normal = 2 + option_anywhere = 3 + default = 1 + + +class DisableCharon(Toggle): + """ + Prevents Charon from taking your money when you re-enter the castle. Also removes Haggling from the Item Pool. + """ + displayname = "Disable Charon" + + +class RequirePurchasing(DefaultOnToggle): + """ + Determines where you will be required to purchase equipment and runes from the Blacksmith and Enchantress before + equipping them. If you disable require purchasing, Manor Renovations are scaled to take this into account. + """ + displayname = "Require Purchasing" + + +class GoldGainMultiplier(Choice): + """ + Adjusts the multiplier for gaining gold from all sources. + """ + displayname = "Gold Gain Multiplier" + option_normal = 0 + option_quarter = 1 + option_half = 2 + option_double = 3 + option_quadruple = 4 + default = 0 + + +class NumberOfChildren(Range): + """ + Determines the number of offspring you can choose from on the lineage screen after a death. + """ + displayname = "Number of Children" + range_start = 1 + range_end = 5 + default = 3 + + +legacy_options: typing.Dict[str, type(Option)] = { + "starting_gender": StartingGender, + "starting_class": StartingClass, + "new_game_plus": NewGamePlus, + "fairy_chests_per_zone": FairyChestsPerZone, + "chests_per_zone": ChestsPerZone, + "vendors": Vendors, + "disable_charon": DisableCharon, + "require_purchasing": RequirePurchasing, + "gold_gain_multiplier": GoldGainMultiplier, + "number_of_children": NumberOfChildren, + "death_link": DeathLink, +} diff --git a/worlds/legacy/Regions.py b/worlds/legacy/Regions.py new file mode 100644 index 00000000..7990da27 --- /dev/null +++ b/worlds/legacy/Regions.py @@ -0,0 +1,60 @@ +import typing + +from BaseClasses import MultiWorld, Region, Entrance +from .Items import LegacyItem +from .Locations import LegacyLocation, diary_location_table, location_table, base_location_table +from .Names import LocationName, ItemName + + +def create_regions(world, player: int): + + locations: typing.List[str] = [] + + # Add required locations. + locations += [location for location in base_location_table] + locations += [location for location in diary_location_table] + + # Add chests per settings. + fairies = int(world.fairy_chests_per_zone[player]) + for i in range(0, fairies): + locations += [f"{LocationName.castle} - Fairy Chest {i + 1}"] + locations += [f"{LocationName.garden} - Fairy Chest {i + 1}"] + locations += [f"{LocationName.tower} - Fairy Chest {i + 1}"] + locations += [f"{LocationName.dungeon} - Fairy Chest {i + 1}"] + + chests = int(world.chests_per_zone[player]) + for i in range(0, chests): + locations += [f"{LocationName.castle} - Chest {i + 1}"] + locations += [f"{LocationName.garden} - Chest {i + 1}"] + locations += [f"{LocationName.tower} - Chest {i + 1}"] + locations += [f"{LocationName.dungeon} - Chest {i + 1}"] + + # Set up the regions correctly. + world.regions += [ + create_region(world, player, "Menu", None, [LocationName.outside]), + create_region(world, player, LocationName.castle, locations), + ] + + # Connect entrances and set up events. + world.get_entrance(LocationName.outside, player).connect(world.get_region(LocationName.castle, player)) + world.get_location(LocationName.castle, player).place_locked_item(LegacyItem(ItemName.boss_khindr, True, None, player)) + world.get_location(LocationName.garden, player).place_locked_item(LegacyItem(ItemName.boss_alexander, True, None, player)) + world.get_location(LocationName.tower, player).place_locked_item(LegacyItem(ItemName.boss_leon, True, None, player)) + world.get_location(LocationName.dungeon, player).place_locked_item(LegacyItem(ItemName.boss_herodotus, True, None, player)) + world.get_location(LocationName.fountain, player).place_locked_item(LegacyItem(ItemName.boss_fountain, True, None, player)) + + +def create_region(world: MultiWorld, player: int, name: str, locations=None, exits=None): + # Shamelessly stolen from the ROR2 definition, lol + ret = Region(name, None, name, player) + ret.world = world + if locations: + for location in locations: + loc_id = location_table.get(location, 0) + location = LegacyLocation(player, location, loc_id, ret) + ret.locations.append(location) + if exits: + for exit in exits: + ret.exits.append(Entrance(player, exit, ret)) + + return ret diff --git a/worlds/legacy/Rules.py b/worlds/legacy/Rules.py new file mode 100644 index 00000000..3dd233ca --- /dev/null +++ b/worlds/legacy/Rules.py @@ -0,0 +1,131 @@ +from BaseClasses import MultiWorld +from .Names import LocationName, ItemName +from ..AutoWorld import LogicMixin +from ..generic.Rules import set_rule + + +class LegacyLogic(LogicMixin): + def _legacy_has_any_vendors(self, player: int) -> bool: + return self.has_any({ItemName.blacksmith, ItemName.enchantress}, player) + + def _legacy_has_all_vendors(self, player: int) -> bool: + return self.has_all({ItemName.blacksmith, ItemName.enchantress}, player) + + def _legacy_has_stat_upgrades(self, player: int, amount: int) -> bool: + count: int = self.item_count(ItemName.health, player) + self.item_count(ItemName.mana, player) + \ + self.item_count(ItemName.attack, player) + self.item_count(ItemName.magic_damage, player) + \ + self.item_count(ItemName.armor, player) + self.item_count(ItemName.equip, player) + return count >= amount + + +def set_rules(world: MultiWorld, player: int): + # Chests + for i in range(0, world.chests_per_zone[player]): + set_rule(world.get_location(f"{LocationName.garden} - Chest {i + 1}", player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(f"{LocationName.tower} - Chest {i + 1}", player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(f"{LocationName.dungeon} - Chest {i + 1}", player), + lambda state: state.has(ItemName.boss_leon, player)) + + # Fairy Chests + for i in range(0, world.fairy_chests_per_zone[player]): + set_rule(world.get_location(f"{LocationName.garden} - Fairy Chest {i + 1}", player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(f"{LocationName.tower} - Fairy Chest {i + 1}", player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(f"{LocationName.dungeon} - Fairy Chest {i + 1}", player), + lambda state: state.has(ItemName.boss_leon, player)) + + # Vendors + if world.vendors[player] == "early": + set_rule(world.get_location(LocationName.castle, player), + lambda state: state._legacy_has_all_vendors(player)) + elif world.vendors[player] == "normal": + set_rule(world.get_location(LocationName.garden, player), + lambda state: state._legacy_has_any_vendors(player)) + elif world.vendors[player] == "anywhere": + pass # it can be anywhere, so no rule for this! + + # Diaries + for i in range(0, 5): + set_rule(world.get_location(f"Diary {i + 6}", player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(f"Diary {i + 11}", player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(f"Diary {i + 16}", player), + lambda state: state.has(ItemName.boss_leon, player)) + set_rule(world.get_location(f"Diary {i + 21}", player), + lambda state: state.has(ItemName.boss_herodotus, player)) + + # Scale each manor location. + set_rule(world.get_location(LocationName.manor_left_wing_window, player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(LocationName.manor_left_wing_roof, player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(LocationName.manor_right_wing_window, player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(LocationName.manor_right_wing_roof, player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(LocationName.manor_left_big_base, player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(LocationName.manor_right_big_base, player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(LocationName.manor_left_tree1, player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(LocationName.manor_left_tree2, player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(LocationName.manor_right_tree, player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(LocationName.manor_left_big_upper1, player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(LocationName.manor_left_big_upper2, player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(LocationName.manor_left_big_windows, player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(LocationName.manor_left_big_roof, player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(LocationName.manor_left_far_base, player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(LocationName.manor_left_far_roof, player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(LocationName.manor_left_extension, player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(LocationName.manor_right_big_upper, player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(LocationName.manor_right_big_roof, player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(LocationName.manor_right_extension, player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(LocationName.manor_right_high_base, player), + lambda state: state.has(ItemName.boss_leon, player)) + set_rule(world.get_location(LocationName.manor_right_high_upper, player), + lambda state: state.has(ItemName.boss_leon, player)) + set_rule(world.get_location(LocationName.manor_right_high_tower, player), + lambda state: state.has(ItemName.boss_leon, player)) + set_rule(world.get_location(LocationName.manor_observatory_base, player), + lambda state: state.has(ItemName.boss_leon, player)) + set_rule(world.get_location(LocationName.manor_observatory_scope, player), + lambda state: state.has(ItemName.boss_leon, player)) + + # Standard Zone Progression + set_rule(world.get_location(LocationName.garden, player), + lambda state: state._legacy_has_stat_upgrades(player, 10) and state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(LocationName.tower, player), + lambda state: state._legacy_has_stat_upgrades(player, 25) and state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(LocationName.dungeon, player), + lambda state: state._legacy_has_stat_upgrades(player, 40) and state.has(ItemName.boss_leon, player)) + + # Bosses + set_rule(world.get_location(LocationName.boss_khindr, player), + lambda state: state.has(ItemName.boss_khindr, player)) + set_rule(world.get_location(LocationName.boss_alexander, player), + lambda state: state.has(ItemName.boss_alexander, player)) + set_rule(world.get_location(LocationName.boss_leon, player), + lambda state: state.has(ItemName.boss_leon, player)) + set_rule(world.get_location(LocationName.boss_herodotus, player), + lambda state: state.has(ItemName.boss_herodotus, player)) + set_rule(world.get_location(LocationName.fountain, player), + lambda state: state._legacy_has_stat_upgrades(player, 50) and state.has(ItemName.boss_herodotus, player)) + + world.completion_condition[player] = lambda state: state.has(ItemName.boss_fountain, player) diff --git a/worlds/legacy/__init__.py b/worlds/legacy/__init__.py new file mode 100644 index 00000000..41e7a20b --- /dev/null +++ b/worlds/legacy/__init__.py @@ -0,0 +1,105 @@ +import typing + +from BaseClasses import Item, MultiWorld +from .Items import LegacyItem, ItemData, item_table, vendors_table, static_classes_table, progressive_classes_table, \ + skill_unlocks_table, blueprints_table, runes_table, misc_items_table +from .Locations import LegacyLocation, location_table, base_location_table +from .Options import legacy_options +from .Regions import create_regions +from .Rules import set_rules +from .Names import ItemName +from ..AutoWorld import World + + +class LegacyWorld(World): + """ + Rogue Legacy is a genealogical rogue-"LITE" where anyone can be a hero. Each time you die, your child will succeed + you. Every child is unique. One child might be colorblind, another might have vertigo-- they could even be a dwarf. + But that's OK, because no one is perfect, and you don't have to be to succeed. + """ + game: str = "Rogue Legacy" + options = legacy_options + topology_present = False + data_version = 1 + + item_name_to_id = {name: data.code for name, data in item_table.items()} + location_name_to_id = location_table + + def _get_slot_data(self): + return { + "starting_gender": self.world.starting_gender[self.player], + "starting_class": self.world.starting_class[self.player], + "new_game_plus": self.world.new_game_plus[self.player], + "fairy_chests_per_zone": self.world.fairy_chests_per_zone[self.player], + "chests_per_zone": self.world.chests_per_zone[self.player], + "vendors": self.world.vendors[self.player], + "disable_charon": self.world.disable_charon[self.player], + "require_purchasing": self.world.require_purchasing[self.player], + "gold_gain_multiplier": self.world.gold_gain_multiplier[self.player], + "number_of_children": self.world.number_of_children[self.player], + "death_link": self.world.death_link[self.player], + } + + def _create_items(self, name: str): + data = item_table[name] + return [self.create_item(name)] * data.quantity + + def fill_slot_data(self) -> dict: + slot_data = self._get_slot_data() + for option_name in legacy_options: + option = getattr(self.world, option_name)[self.player] + slot_data[option_name] = option.value + + return slot_data + + def generate_basic(self): + itempool: typing.List[LegacyItem] = [] + total_required_locations = 61 + (self.world.chests_per_zone[self.player] * 4) + (self.world.fairy_chests_per_zone[self.player] * 4) + + # Fill item pool with all required items + for item in {**skill_unlocks_table, **blueprints_table, **runes_table}: + # if Haggling, do not add if Disable Charon. + if item == ItemName.haggling and self.world.disable_charon[self.player] == 1: + continue + itempool += self._create_items(item) + + # Add specific classes into the pool. Eventually, will be able to shuffle the starting ones, but until then... + itempool += [ + self.create_item(ItemName.paladin), + self.create_item(ItemName.archmage), + self.create_item(ItemName.barbarian_king), + self.create_item(ItemName.assassin), + self.create_item(ItemName.dragon), + self.create_item(ItemName.traitor), + *self._create_items(ItemName.progressive_shinobi), + *self._create_items(ItemName.progressive_miner), + *self._create_items(ItemName.progressive_lich), + *self._create_items(ItemName.progressive_spellthief), + ] + + # Check if we need to start with these vendors or put them in the pool. + if self.world.vendors[self.player] == "start_unlocked": + self.world.push_precollected(self.world.create_item(ItemName.blacksmith, self.player)) + self.world.push_precollected(self.world.create_item(ItemName.enchantress, self.player)) + else: + itempool += [self.create_item(ItemName.blacksmith), self.create_item(ItemName.enchantress)] + + # Add Arcitect. + itempool += [self.create_item(ItemName.architect)] + + # Fill item pool with the remaining + for _ in range(len(itempool), total_required_locations): + item = self.world.random.choice(list(misc_items_table.keys())) + itempool += [self.create_item(item)] + + self.world.itempool += itempool + + def create_regions(self): + create_regions(self.world, self.player) + + def create_item(self, name: str) -> Item: + data = item_table[name] + return LegacyItem(name, data.progression, data.code, self.player) + + def set_rules(self): + set_rules(self.world, self.player)